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<>();
为例,这个 map
的 key
就是 localVar
,value
就是一个字符串,这个字符串是在每一个线程中,通过 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=null
的Entry
的value
还存在一个强引链 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
,防止内存泄露。