Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does this ThreadLocal prevent the Classloader from getting GCed

I am trying to understand how Threadlocal can cause Classloader leak. So for this I have this code

public class Main {
    public static void main(String... args) throws Exception {
        loadClass();
        while (true) {
            System.gc();
            Thread.sleep(1000);
        }
    }

    private static void loadClass() throws Exception {
        URL url = Main.class.getProtectionDomain()
                .getCodeSource()
                .getLocation();
        MyCustomClassLoader cl = new MyCustomClassLoader(url);
        Class<?> clazz = cl.loadClass("com.test.Foo");
        clazz.newInstance();
        cl = null;
    }
}

class MyCustomClassLoader extends URLClassLoader {
    public MyCustomClassLoader(URL... urls) {
        super(urls, null);
    }

    @Override
    protected void finalize() {
        System.out.println("*** CustomClassLoader finalized!");
    }
}

Foo.java

public class Foo {
    private static final ThreadLocal<Bar> tl = new ThreadLocal<Bar>();

    public Foo() {
        Bar bar = new Bar();
        tl.set(bar);
        System.out.println("Test ClassLoader: " + this.getClass()
                .getClassLoader());
    }

    @Override
    protected void finalize() {
        System.out.println(this + " finalized!");
    }
}

Bar.java

public class Bar {
    public Bar() {
        System.out.println(this + " created");
        System.out.println("Bar ClassLoader: " + this.getClass()
                .getClassLoader());
    }

    @Override
    public void finalize() {
        System.out.println(this + " finalized");
    }
}

After running this code it shows that MyCustomClassloader and Bar finalize is not called, only Foo finalize is called. But when I change Threadlocal to String then all the finalize is called.

public class Foo {
    private static final ThreadLocal<String> tl = new ThreadLocal<String>();

    public Foo() {
        Bar bar = new Bar();
        tl.set("some");
        System.out.println("Test ClassLoader: " + this.getClass()
                .getClassLoader());
    }

Can you please explain why there is a difference when using ThreadLocal as String vs Bar?

like image 929
Pakira Avatar asked Oct 15 '22 22:10

Pakira


1 Answers

When you set the thread local variable to an instance of Bar, the value has an implicit reference to its defining class loader, which is also the defining class loader of Foo and hence, has an implicit reference to its static variable tl holding the ThreadLocal.

In contrast, the String class is defined by the bootstrap loader and has no implicit reference to the the Foo class.

Now, a reference cycle is not preventing garbage collection per se. If only one object holds a reference to a member of the cycle and that object becomes unreachable, the entire cycle would become unreachable. The problem here is that the object still referencing the cycle is the Thread that is still alive.

The specific value is associated with the combination of a ThreadLocal instance and a Thread instance and we’d wish that if either of them becomes unreachable, it would stop referencing the value. Unfortunately, no such feature exists. We can only associate a value with the reachability of one object, like with the key of a WeakHashMap, but not of two.

In the OpenJDK implementation, the Thread is the owner of this construct, which makes it immune against values back-referencing the Thread. E.g.

ThreadLocal<Thread> local = new ThreadLocal<>();

ReferenceQueue<Thread> q = new ReferenceQueue<>();

Set<Reference<?>> refs = ConcurrentHashMap.newKeySet();

new Thread(() -> {
    Thread t = Thread.currentThread();
    local.set(t);
    refs.add(new WeakReference<>(t, q));
}).start();

Reference<?> r;
while((r = q.remove(2000)) == null) {
    System.gc();
}

if(refs.remove(r)) System.out.println("Collected");
else System.out.println("Something very suspicuous is going on");

This will print Collected, indicating that the reference from the value to the Thread did not prevent the removal, unlike put(t, t) on a WeakHashMap.

The price is that this construct is not immune against backreferences to the ThreadLocal instance.

ReferenceQueue<Object> q = new ReferenceQueue<>();

Set<Reference<?>> refs = ConcurrentHashMap.newKeySet();

createThreadLocal(refs, q);

Reference<?> r;
while((r = q.remove(2000)) == null) {
    System.gc();
}

if(refs.remove(r)) System.out.println("Collected");
else System.out.println("Something very suspicuous is going on");
static void createThreadLocal(Set<Reference<?>> refs, ReferenceQueue<Object> q) {
    ThreadLocal<ThreadLocal<?>> local = new ThreadLocal<>();
    local.set(local);
    refs.add(new WeakReference<>(local, q));
}

This will hang forever, as the backreference from the ThreadLocal to itself prevents its garbage collection, as long as the associated thread is still alive.

Your case is just a special variant of it, as the backreference is through the Bar instance, its defining loader, to Foo’s static variable. But the principle is the same.

You only need to change the line

loadClass();

to

new Thread(new FutureTask(() -> { loadClass(); return null; })).start();

to stop the value from being associated with the main thread. Then, the class loader and all associated classes and instances get garbage collected.

like image 138
Holger Avatar answered Oct 17 '22 12:10

Holger