Java WeakReference 弱引用

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 = nullsb2 = 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的弱引用

ThreadLocalThreadLocalMap.Entry 的 key, 而 ThreadLocalMap.EntryThreadLocal 的引用是 弱引用.

**当我们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源码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值