这个ThreadLocal如何防止类加载器被GCed

Pak*_*ira 5 java multithreading garbage-collection memory-leaks

我想了解如何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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

酒吧.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");
    }
}
Run Code Online (Sandbox Code Playgroud)

运行此代码后,它显示未调用 Finalize,仅MyCustomClassloader 调用了 Finalize。但是当我将 Threadlocal 更改为 String 时,所有的finalize都会被调用。BarFoo

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());
    }
Run Code Online (Sandbox Code Playgroud)

String您能解释一下为什么使用 ThreadLocal as和 ThreadLocal时会有差异吗Bar

Hol*_*ger 3

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

\n

相反,该类String是由引导加载程序定义的,并且没有对该Foo类的隐式引用。

\n

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

\n

ThreadLocal特定值与实例和实例的组合相关联Thread,我们\xe2\x80\x99d希望如果其中任何一个变得无法访问,它将停止引用该值。不幸的是,不存在这样的功能。我们只能将一个值与一个对象的可达性相关联,就像与 a 的键相关联WeakHashMap,但不能与两个对象的键相关联。

\n

在 OpenJDK 实现中,Thread是此构造的所有者,这使其免受反向引用Thread. 例如

\n
ThreadLocal<Thread> local = new ThreadLocal<>();\n\nReferenceQueue<Thread> q = new ReferenceQueue<>();\n\nSet<Reference<?>> refs = ConcurrentHashMap.newKeySet();\n\nnew Thread(() -> {\n    Thread t = Thread.currentThread();\n    local.set(t);\n    refs.add(new WeakReference<>(t, q));\n}).start();\n\nReference<?> r;\nwhile((r = q.remove(2000)) == null) {\n    System.gc();\n}\n\nif(refs.remove(r)) System.out.println("Collected");\nelse System.out.println("Something very suspicuous is going on");\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

代价是这个构造不能避免对实例的反向引用ThreadLocal

\n
ReferenceQueue<Object> q = new ReferenceQueue<>();\n\nSet<Reference<?>> refs = ConcurrentHashMap.newKeySet();\n\ncreateThreadLocal(refs, q);\n\nReference<?> r;\nwhile((r = q.remove(2000)) == null) {\n    System.gc();\n}\n\nif(refs.remove(r)) System.out.println("Collected");\nelse System.out.println("Something very suspicuous is going on");\n
Run Code Online (Sandbox Code Playgroud)\n
static void createThreadLocal(Set<Reference<?>> refs, ReferenceQueue<Object> q) {\n    ThreadLocal<ThreadLocal<?>> local = new ThreadLocal<>();\n    local.set(local);\n    refs.add(new WeakReference<>(local, q));\n}\n
Run Code Online (Sandbox Code Playgroud)\n

ThreadLocal这将永远挂起,因为只要关联的线程仍然处于活动状态,从 到 自身的反向引用就会阻止其垃圾回收。

\n

您的情况只是它的一个特殊变体,因为反向引用是通过Bar实例(其定义加载程序)到Foo\xe2\x80\x99sstatic变量。但原理是一样的。

\n

你只需要改变线路

\n
loadClass();\n
Run Code Online (Sandbox Code Playgroud)\n

\n
new Thread(new FutureTask(() -> { loadClass(); return null; })).start();\n
Run Code Online (Sandbox Code Playgroud)\n

停止该值与主线程关联。然后,类加载器以及所有关联的类和实例都会被垃圾收集。

\n