Java Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we'll learn how garbage collection treats static fields. Also, we'll touch on such topics as class loading and class objects. After this article, we'll understand better the connection between classes, classloaders, and static fields and how the garbage collector treats them.

2. Overview of Garbage Collection in Java

Java provides a pretty nice feature of automatic memory management. This approach, in most cases, isn't as efficient as the manual. However, it helps to avoid hard-to-debug problems and reduces boilerplate code. Also, with the improvements in garbage collection, the process becomes better and better. Thus, we should review how the garbage collector works and what garbage is in our applications.

2.1. Garbage Objects

Reference counting is the most straightforward and intuitive way to identify garbage objects. This approach allows us to check if our current object has any references to it. However, this approach has some drawbacks, and the most significant one is cyclic references.

One of the methods to deal with cyclic references is tracing. Objects turn into garbage when they don't have any links to the garbage collection roots of the application.

2.2. Static Fields and Class Objects

In Java, everything is an Object, including the definition of classes. They contain all the meta information about a class, methods, and the values of static fields. Thus, all the static fields are references by respected class objects. Therefore, until the class object exists and references by the application, static fields won't be eligible for garbage collection.

At the same time, all the loaded classes have the reference to the classloader used for loading this particular class. This way, we can keep track of the loaded classes.

In this case, we have a hierarchy of references. A classloader keeps the reference to all the loaded classes. At the same time, classes store the reference to the respective classloader. We have two-way referencing in this case. Whenever we're instantiating a new object, it will hold a reference to the definition of its classes. Thus, we have the following hierarchy:

We cannot unload a class until our application has a reference to it. Let's check what we need to make a class definition eligible for garbage collection. First, there should be no references from an application to the instances of a class. It's important because all the instances bear references to their class. Second, the class loader for this class should be unavailable from an application. And lastly, the class itself should have no references in an application.

3. Example of a Garbage Collected Static Field

Let's create an example in which we make a garbage collector remove our static field. JVM supports class unloading for the classes loaded by extension and system class loaders. However, this will be hard to reproduce, and we'll use a custom class loader for this as we'll have more control over it.

3.1. Custom Class Loader

First, let's create our own CustomClassloader will load a class from a resource folder of our application. For our classloader to work, we should override the loadClass(String name) method:

public class CustomClassloader extends ClassLoader {

    public static final String PREFIX = "com.baeldung.classloader";

    public CustomClassloader(ClassLoader parent) {
        super(parent);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.startsWith(PREFIX)) {
            return getClass(name);
        } else {
            return super.loadClass(name);
        }
    }

    ...
}

In this implementation, we're using the getClass method, which hides the complexity of loading a class from resources:

private Class<?> getClass(String name) {
    String fileName = name.replace('.', File.separatorChar) + ".class";
    try {
        byte[] byteArr = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream(fileName));
        Class<?> c = defineClass(name, byteArr, 0, byteArr.length);
        resolveClass(c);
        return c;
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

3.2. Folder Structure

To work correctly, our custom class should be outside the scope of our class path. This way, it won't be uploaded by a system class loader. The only classloader working with this particular class will be our CustomClassloader.

The structure of our folders will look like this:

3.3. Static Field Holder

We'll use a custom class that will play the role of a holder for our static field. After defining our implementation of a classloader, we can use it to upload a class we've prepared. It's a simple class:

public class GarbageCollectedStaticFieldHolder {

    private static GarbageCollectedInnerObject garbageCollectedInnerObject =
      new GarbageCollectedInnerObject("Hello from a garbage collected static field");

    public void printValue() {
        System.out.println(garbageCollectedInnerObject.getMessage());
    }
}

3.4. Static Field Class

GarbageCollectedInnerObject will represent an object that we want to turn into garbage. This class, for simplicity and convenience defined in the same file as GarbageCollectedStaticFieldHolder. This class contains a message and also has overridden finalize() method. Although the finalize() method is deprecated and has many drawbacks, it will allow us to visualize when the garbage collector removes the object. We'll use this method only for presentation purposes. Here's our class for a static field:

class GarbageCollectedInnerObject {

    private final String message;

    public GarbageCollectedInnerObject(final String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    @Override
    protected void finalize() {
        System.out.println("The object is garbage now");
    }
}

3.5. Uploading a Class

Now we can upload and instantiate our class. After creating an instance, we can ensure that the class was uploaded, the object created, and the static field contains the needed information:

private static void loadClass() {
    try {
        final String className = "com.baeldung.classloader.GarbageCollectedStaticFieldHolder";
        CustomClassloader loader = new CustomClassloader(Main.class.getClassLoader());
        Class<?> clazz = loader.loadClass(className);
        Object instance = clazz.getConstructor().newInstance();
        clazz.getMethod(METHOD_NAME).invoke(instance);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

This method should create the instance of our special class and output the message:

Hello from a garbage collected static field

3.6. Garbage Collection in Action

Now let's start our application and try to remove the garbage:

public static void main(String[] args) throws InterruptedException {
    loadClass();
    System.gc();
    Thread.sleep(1000);
}

After calling the method loadClass(), all the variables inside this method, namely, classloader, our class, and instance, will go out of scope and lose the connection with the garbage collection roots. It's also possible to assign null to the references, but the option to use scope is cleaner:

public static void main(String[] args) throws InterruptedException {
    CustomClassloader loader;
    Class<?> clazz;
    Object instance;
    try {
        final String className = "com.baeldung.classloader.GarbageCollectedStaticFieldHolder";
        loader = new CustomClassloader(GarbageCollectionNullExample.class.getClassLoader());
        clazz = loader.loadClass(className);
        instance = clazz.getConstructor().newInstance();
        clazz.getMethod(METHOD_NAME).invoke(instance);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    loader = null;
    clazz = null;
    instance = null;
    System.gc();
    Thread.sleep(1000);
}

Even though we have some problems with this code, it'll work in most cases. The main issue is that we cannot force garbage collection in Java, and the invocation of  System.gc() won't guarantee that the garage collection will happen. However, in most JVM implementations, this will trigger Major Garbage Collection. Thus, we should see the following lines in the output:

Hello from a garbage collected static field 
The object is garbage now

This output shows us that the garbage collector removed the static field. The garbage collector also removed the classloader, the classes for the holder, the static field, and the connected objects.

3.7. Example Without System.gc()

We also can trigger garbage collection more naturally. This way will work more stable. However, it will require more cycles to invoke the garbage collector:

public static void main(String[] args) {
    while (true) {
        loadClass();
    }
}

Here we're using the same loadClass() method, but we don't invoke System.gc(), and the garbage collector is triggered when we run out of memory because we're loading the class in an infinite loop.

4. Conclusion

This article taught us how garbage collection works in Java regarding classes and static fields. We created a custom class loader and used it for our example. Also, we learned the connection between classloaders, classes, and their static fields. For further understanding, it's worth going through the articles linked in the texts.

The code is available over on GitHub.

Java bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Generic footer banner
guest
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments