这个 ThreadLocal 如何防止 Classloader 被 GCed

How does this ThreadLocal prevent the Classloader from getting GCed

我正在尝试了解 Threadlocal 如何导致 Classloader 泄漏。所以为此我有这个代码

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");
    }
}

在 运行 这段代码之后表明 MyCustomClassloader Bar finalize 没有被调用,只有 Foo finalize 被调用。 但是当我将 Threadlocal 更改为 String 时,所有的最终确定都会被调用。

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());
    }

您能否解释一下为什么将 ThreadLocal 用作 StringBar 时存在差异?

当您将线程局部变量设置为 Bar 的实例时,该值具有对其定义的 class 加载器的隐式引用,这也是定义 class 加载器的Foo 因此,隐式引用了它的 static 变量 tl 持有 ThreadLocal.

相比之下,String class 由 bootstrap 加载程序定义,并且没有隐式引用 Foo class。

现在,引用循环本身并不能阻止垃圾收集。如果只有一个对象持有对循环成员的引用并且该对象变得不可访问,则整个循环将变得不可访问。这里的问题是仍然引用循环的对象是仍然存在的Thread

特定值与 ThreadLocal 实例和 Thread 实例的组合相关联,我们希望如果 任一个 成为无法访问,它将停止引用该值。不幸的是,不存在这样的功能。我们只能将一个值与一个对象的可达性相关联,例如 WeakHashMap 的键,而不是两个

的键。

在 OpenJDK 实现中,Thread 是此构造的所有者,这使得它不受值反向引用 Thread 的影响。例如

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");

这将打印 Collected,表明从值到 Thread 的引用并没有阻止删除,这与 WeakHashMap 上的 put(t, t) 不同。

代价是此构造无法避免对 ThreadLocal 实例的反向引用。

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));
}

这将永远挂起,因为从 ThreadLocal 到自身的反向引用阻止了它的垃圾收集,只要关联的线程仍然存在。

你的案例只是它的一个特殊变体,因为反向引用是通过 Bar 实例,它的定义加载器,到 Foostatic 变量。但是原理是一样的。

你只需要换行

loadClass();

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

阻止该值与主线程相关联。然后,class 加载器和所有关联的 classes 和实例被垃圾收集。