这个 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 用作 String
与 Bar
时存在差异?
当您将线程局部变量设置为 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
实例,它的定义加载器,到 Foo
的 static
变量。但是原理是一样的。
你只需要换行
loadClass();
至
new Thread(new FutureTask(() -> { loadClass(); return null; })).start();
阻止该值与主线程相关联。然后,class 加载器和所有关联的 classes 和实例被垃圾收集。
我正在尝试了解 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 用作 String
与 Bar
时存在差异?
当您将线程局部变量设置为 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
实例,它的定义加载器,到 Foo
的 static
变量。但是原理是一样的。
你只需要换行
loadClass();
至
new Thread(new FutureTask(() -> { loadClass(); return null; })).start();
阻止该值与主线程相关联。然后,class 加载器和所有关联的 classes 和实例被垃圾收集。