Loading

Java 引用

date: 2020-12-08 15:42:56
updated: 2020-12-08 17:27:04

Java 引用

1. 引用类型

  • 强引用
    • 栈 -> 堆
    • 只要堆中对象可达,就不会被回收
  • 软引用
    • 需要 import SoftReference
    • 栈 -> 堆中会创建一个 SoftReference对象,这个对象里会有一个value -> 这个value指向了堆中的一个对象,也就是真正的值
    • 当堆中的内存不够的时候,软引用指向的对象就会被回收
    • 应用:适合做缓存,比如图片
  • 弱引用
    • 需要 import WeakReference
    • 栈 -> 堆中会创建一个 WeakReference对象,这个对象里会有一个value -> 这个value指向了堆中的一个对象,也就是真正的值
    • 当弱引用遇到gc就会被回收
    • 应用:为了解决内存泄露问题,比如ThreadLocal
  • 虚引用
    • 需要 import PhantomReference
    • 应用:管理堆外内存。流程大概如下:OS读取数据,会放在内存里,之前的处理方式(比如BIO)会把这一部分内存复制到JVM内存中,来回复制,一方面是效率低下,另一方面容易造成内存溢出,因为JVM无法及时清理外面的内存。现在通过虚引用(NIO中的DirectByteBuffer)可以直接在JVM中创建一个指向堆外内存的对象,当JVM中的对象被回收的时候(应该就是处理完数据了),回收这个动作可以通过监听Queue探测到,这时候就可以由GC来回收堆外内存的对象。

2. ThreadLocal中的弱引用

ThreadLocal的设计初衷:提供线程内部的局部变量,在本线程内可以随意使用,隔离其他线程

每一个Thread对象,都包含一个 ThreadLocal.ThreadLocalMap threadLocals 的属性。

static ThreadLocal<String> localVar = new ThreadLocal<>();为例,这个 mapkey 就是 localVarvalue 就是一个字符串,这个字符串是在每一个线程中,通过 localVar.set("xxx") 设置的

ThreadLocal 在 get、set 的时候会首先获取到 Thread.currentThread(),然后再根据线程拿到 threadLocals 这个map,然后在map中进行操作,保证了只是针对当前线程的变量进行操作

ThreadLocalMap 中的每一对 key,value 都是存放在 Entry 中的,而 Entry 继承了 WeakReference,并在构造函数中将 key 作为了弱引用

/**
 * The entries in this hash map extend WeakReference, using
 * its main ref field as the key (which is always a
 * ThreadLocal object).  Note that null keys (i.e. entry.get()
 * == null) mean that the key is no longer referenced, so the
 * entry can be expunged from table.  Such entries are referred to
 * as "stale entries" in the code that follows.
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
  /** The value associated with this ThreadLocal. */
  Object value;

  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}

为什么要用到弱引用?避免内存溢出。为啥会内存溢出?

Thread --> ThreadLocal.ThreadLocalMap<localVar, "xxx"> 其中 localVar 是弱引用

Thread 中含有指向 ThreadLocal 类下的 ThreadLocalMap 这个对象的变量

创建一个 ThreadLocal<String> localVar,如果不是弱引用, localVar = null,对应的ThreadLocalMap中的key就是null,理论上应该回收 ``ThreadLocal对象,但是并不会,因为ThreadLocal.ThreadLocalMap 还被某个线程强引用(生产上的线程多数都是一直在运行的),就会导致ThreadLocalMap中的内存一直无法被回收。现在是弱引用,即 localVar = null`,如果这时候GC扫描到了就可以回收,哪怕线程正在进行。

简单来说就是,ThreadLocalMap 这块内存,除了 ThreadLocal 指向它,Thread 也指向它,ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value

仅仅是把key置为null是不够的,因为value还是不会被回收掉,key=nullEntryvalue还存在一个强引链 Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致内存泄漏。所以正确的做法是

static ThreadLocal<String> localVar = new ThreadLocal<>();
localVar.set("zhangsan");
localVar.remove();	// 调用remove()方法删除entry,底层调用expungeStaleEntry()方法,如果key=null,就把value置为null;如果key不为null,就通过开放寻址法将kv从Entry[] tab里移除

在ThreadLocal的get、set方法以及扩容时,会清理掉key=null的Entry

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
     Entry[] tab = table;
     int len = tab.length;

     while (e != null) {
         ThreadLocal<?> k = e.get();
         if (k == key)
             return e;
         if (k == null)
             expungeStaleEntry(i);
         else
             i = nextIndex(i, len);
         e = tab[i];
     }
     return null;
 }

首先在索引位置去拿到一个Entry e,如果e不为null并且key相同返回e;如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询。

虽然ThreadLocal本身也做了避免内存泄露的优化,但是上述成功前提是需要调用get、set方法 => 大多数情况下还是手动调用 remove() 更好 => JDK 建议就是把 ThreadLocal 变量定义成private static的,这样的话ThreadLocal的生命周期就更长,就会一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,所以我们需要调用 remove,防止内存泄露。

posted @ 2020-12-19 11:18  猫熊小才天  阅读(113)  评论(0编辑  收藏  举报