这个 ThreadLocal 如何防止 Classloader 被垃圾回收?

5

我试图理解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");
    }
}

运行此代码后,显示只有Foo的finalize方法被调用,MyCustomClassloaderBar的finalize方法未被调用。但是当我将ThreadLocal更改为String时,所有的finalize方法都被调用。
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时会有差异?


如果您将其存储在ThreadLocal中,为什么会被finalized?因为您已经将其存储在仍然可达的位置(因为“Foo”仍在加载中)。如果您将字符串存储在ThreadLocal中,则不会存储Bar,因此它是不可访问的并且可以被垃圾回收。 - Andy Turner
1
乍一看,这并不像看起来那么简单。 - Holger
1个回答

3

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

相比之下,String类由引导加载器定义,不会隐式引用Foo类。

现在,引用循环本身并没有阻止垃圾回收。如果只有一个对象持有循环成员的引用,并且该对象变得不可访问,整个循环将变得不可访问。问题在于仍然引用循环的对象是仍然存活的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();

to

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

防止值与主线程关联,然后类加载器、所有相关的类和实例就会被垃圾回收。


谢谢您提供的详细信息,能否请您解释一下“防止该值与主线程相关联”的含义?这是因为新线程没有对主线程的引用吗? - Pakira
1
您正在设置一个线程本地变量的值。该值将与执行tl.set(bar);语句的线程相关联。当主线程执行该语句时,该值将与主线程相关联,因为它是在循环中调用System.gc()的线程之一。当不同的线程执行该语句时,该值将与该特定线程相关联,并且可以在该线程终止时删除。 - Holger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接