Java中的引用类型
Java中存在四种引用, 它们由强到弱依次是: 强引用, 软引用, 弱引用, 虚引用.
除了 强引用 外, 其他3中引用均可在 java.lang.ref
包中找到.
强引用(Strong Reference)
程序中默认使用的引用类型, 若一个对象通过一系列强引用可到达, 它就是强可达的(strongly reachable), 那么它就不被回收.
下面是一个强引用示例:
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("hello world");
StringBuilder sb2 = sb;
if (sb == sb2) {
System.out.println("Heap addresses are identical.");
}
System.gc();
// StringBuilder 对象有2个强引用, sb 和 sb2
System.out.println("After gc, still strongly reachable[2], sb2: " + sb2);
sb = null;
System.gc();
// StringBuilder 对象有1个强引用, sb2
System.out.println("After gc, still strongly reachable[1], sb2: " + sb2);
sb2 = null;
System.gc();
// StringBuilder 对象没有强引用, StringBuilder 对象的堆内存空间会被 GC 回收
System.out.println("After gc, unreachable[0], sb2: " + sb2);
}
程序输出结果:
Heap addresses are identical.
After gc, still strongly reachable[2], sb2: hello world
After gc, still strongly reachable[1], sb2: hello world
After gc, unreachable[0], sb2: null
局部变量表 是 栈帧 的重要组成部分(具体细节请参考Java虚拟机内存模型).
当 sb = null
和 sb2 = null
执行后, 两条 强引用(黄线) 就断了.
软引用(Soft Reference)
可被回收的引用.
软引用是比强引用弱一点的引用类型.
如果一个对象只持有软引用, 那么, 当堆空间不足时, 就会被回收对象的内存.
弱引用(Weak Reference)
发现即回收.
弱引用是一种比软引用弱的引用类型.
在发生 GC 时, 只要发现弱引用, 不管堆空间使用情况如何, 都会将对象的内存回收.
但是, 由于垃圾回收器的线程优先级通常很低, 所以并不一定很快发现持有弱引用的对象.
弱引用示例:
public class User {
private String id;
private String name;
private int age;
// omit constructors and getter/setter
}
public static void main(String[] args) {
// user 是 "对象user" 的强引用
User user = new User("123", "mitre", 21);
// weakRef 是 "对象user" 的弱引用
WeakReference<User> weakRef = new WeakReference<>(user);
System.out.println("弱引用 和 强引用 同时指向的内存区域:" + weakRef.get());
System.out.println("弱引用 和 强引用 同时指向的内存区域:" + user);
System.out.println("---GC---");
System.gc();
System.out.println("弱引用 和 强引用 同时指向的内存区域:" + weakRef.get());
System.out.println("弱引用 和 强引用 同时指向的内存区域:" + user);
System.out.println("\n移除user对象持有的强引用");
user = null;
System.out.println("---GC---");
System.gc();
System.out.println("只有 弱引用 指向的内存区域:" + weakRef.get());
}
结果:
弱引用 和 强引用 同时指向的内存区域:User{id='123', name='mitre', age=21}
弱引用 和 强引用 同时指向的内存区域:User{id='123', name='mitre', age=21}
---GC---
弱引用 和 强引用 同时指向的内存区域:User{id='123', name='mitre', age=21}
弱引用 和 强引用 同时指向的内存区域:User{id='123', name='mitre', age=21}
移除user对象持有的强引用
---GC---
只有 弱引用 指向的内存区域:null
结论:
只持有弱引用的对象, 在GC时, 会被回收内存空间(对象会被清除).
虚引用(Phantom Reference)
对象回收跟踪.
虚引用是引用类型中最弱的. 一个持有虚引用的对象, 和没有引用几乎一样. 当试图通过虚引用的 get()
方法取得对象时, 总会失败.
虚引用必须和引用队列一起使用, 用于跟踪垃圾回收的过程.
ThreadLocalMap.Entry referent的弱引用
ThreadLocal
是 ThreadLocalMap.Entry
的 key, 而 ThreadLocalMap.Entry
对 ThreadLocal
的引用是 弱引用.
**当我们new ThreadLocal<>()
时, ThreadLocal 对象会被 Thread.currentThread().threadLocals.table
弱引用. **
如果一个 ThreadLocal 对象 没有 持有其他引用, 而 只持有 ThreadLocalMap 的弱引用, 那么在GC时, ThreadLocal 对象会被回收, ThreadLocalMap 的 key 会被置为 null
.
备注:
真正持有 ThreadLocal 弱引用的是 ThreadLocalMap.Entry, 由于 Entry[] table
是 ThreadLocalMap 的成员遍变量, 而其 ThreadLocalMap 的可见性是 package private 的(正常情况下, 用户无法引用 ThreadLocalMap), 所以可以简单的把 ThreadLocal 理解为 ThreadLocalMap 的弱引用.
示例:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
System.out.println("case 1: ");
Thread t = new Thread(() -> test(new User("111", "mitre", 11), false));
t.start();
t.join();
System.out.println("\ncase 2: ");
Thread t2 = new Thread(() -> test(new User("222", "rosie", 22), true));
t2.start();
t2.join();
System.out.println("\ncase 3: ");
// 具有强引用的user对象
User user = new User("333", "abby", 33);
// 把具有强引用的user对象传给test方法
Thread t3 = new Thread(() -> test(user, true));
t3.start();
t3.join();
System.out.println(user);
}
private static void test(User user, boolean isGC) {
try {
// 没有其他引用, 下面的 ThreadLocal 对象只持有 ThreadLocalMap 的弱引用
new ThreadLocal<User>().set(user);
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(t);
Class<?> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.printf("弱引用key: %s, 值: %s %n", referenceField.get(o), valueField.get(o));
}
}
if (isGC) {
System.out.println("---GC---");
System.gc();
arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.printf("弱引用key: %s, 值: %s %n", referenceField.get(o), valueField.get(o));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
结果:
case 1:
弱引用key: java.lang.ThreadLocal@3a56ca2a, 值: User{id='111', name='mitre', age=11}
弱引用key: java.lang.ThreadLocal@11d93f44, 值: java.lang.ref.SoftReference@6beb2c06
case 2:
弱引用key: java.lang.ThreadLocal@b59081, 值: User{id='222', name='rosie', age=22}
---GC---
弱引用key: null, 值: User{id='222', name='rosie', age=22}
case 3:
弱引用key: java.lang.ThreadLocal@2fce1d53, 值: User{id='333', name='abby', age=33}
---GC---
弱引用key: null, 值: User{id='333', name='abby', age=33}
User{id='333', name='abby', age=33}
分析:
test
方法中的 ThreadLocal
对象 只持有ThreadLocalMap的弱引用, 所以 gc 后, ThreadLocal 对象会被回收.
不管 ThreadLocal 的 set
方法的实参是啥引用, ThreadLocal 对象都会被回收.
通常我们使用 ThreadLocal 都是用 常量, 这种情况下 ThreadLocal 不会被回收.
例如:
static final ThreadLocal<User> threadLocalStatic = new ThreadLocal<>();
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
// threadLocalStatic 作为常量, 一直强引用 ThreadLocal对象, 所以 ThreadLocal对象 不会被回收
threadLocalStatic.set(new User("444", "Sia", 44));
System.gc();
System.out.println(threadLocalStatic.get());
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(t);
Class<?> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.printf("弱引用key: %s, 值: %s %n", referenceField.get(o), valueField.get(o));
}
}
}
结果:
User{id='444', name='Sia', age=44}
弱引用key: java.lang.ThreadLocal@5fd0d5ae, 值: java.lang.ref.SoftReference@2d98a335
弱引用key: java.lang.ThreadLocal@16b98e56, 值: [Ljava.lang.Object;@7ef20235
弱引用key: java.lang.ThreadLocal@27d6c5e0, 值: java.lang.ref.SoftReference@4f3f5b24
弱引用key: java.lang.ThreadLocal@15aeb7ab, 值: User{id='444', name='Sia', age=44}
弱引用key: java.lang.ThreadLocal@11f103dd, 值: java.lang.ref.SoftReference@7b23ec81
结论:
通常程序中的线程都是线程池里的, 一般不会被销毁, 而 ThreadLocal 又经常被定义为常量, 这可能会存在内存泄漏, 但是这种内存泄漏风险不高, 因为既然 ThreadLocal 都被定义为常量了, 说明某一类工作线程都会使用它, 而同一个 ThreadLocal 对象的 hashcode 是相同的, 所以同一个线程总是会用新值覆盖旧值.
虽然风险不高, 但是为了程序优雅, 建议在使用完 ThreadLocal 变量以后, 使用其 remove
方法清除 ThreadLocalMap 里的无效 Entity.
参考
[1]. 深入理解Java弱引用
[2]. 实战Java虚拟机(葛一鸣著)
[3]. 听说你看过ThreadLocal源码