本文结合,ThreadLocal
内存泄漏 和 DirectByteBuffer
释放 讲解 Java 中的 Reference
。
四种引用类型
- 强引用(Strong Reference):被强引用的对象,GC不能够收集。常见得强引用对象得方式有: 赋值
Object obj = new Object()
,集合引用list.add(new Object())
等等。 - 软引用(Soft Reference):被软引用的对象,GC会在即将发生内存溢出时,只要没有对它强引用,就把它纳入GC收集对象内,进行回收,软引用通过
SoftReference
来实现,它有两个构造(T reference)
和(T reference,ReferenceQueue<? super T> queue)
,和一个get()
方法,用于获取引用的对象。 - 弱引用(Weak Reference):被弱引用得对象,下一次GC时,只要没有对它强引用就会纳入GC收集对象内,进行回收。与
SoftReference
相同,有两个构造和一个获取引用对象得方法。 - 虚引用(Phantom Reference):被虚引用得对象随时可以被GC,并且它不能通过
get()
获取到引用对象,这个方法固定返回为null
,存在得意义在于,可以在对象被收集时,‘得到通知’ 进而做一些其他工作,例如,DirectByteBuffer
就是利用PhantomReference
做直接内存得释放工作得。
Reference
强引用(Strong Reference)底层实现无法感知,其他三种(Soft/Weak/Phantom Reference)均继承于 abstract class Reference<T>
,他们的两个 构造方法 和 一个 获取引用对象 的方法也 均来自于 Reference
。
// SoftReference 简单实现如下
public class SoftReference<T> extends Reference<T> {
public SoftReference(T referent) {
//在构造 Soft/Weak/Phantom Reference 时,一般都需要掉用父级得回调。
super(referent);
this.timestamp = clock;
}
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
Refernece 状态
- Active:最初状态,被 GC 特殊处理,当引用可达性发生变化时,状态会变为
Pending
或者Inactive
,具体是那个状态,依据于这个Referece
在创建的时候是否,绑定了一个ReferenceQueue
。 - Pending:在这个状态时,
Reference
内部变量Reference<Object> pending
会被赋值为当前的引用(这个赋值操作是 JVM 负责的) ,由内部启动得线程掉用它得enqueu
方法进入另一个状态。 - Enqueue:将
Reference<Object> pending
放入到ReferenceQueue
内并唤醒,所有在这个队列上等待得线程, - Inactive:终态,到此为止,这个
Reference
再也不能更改其他状态了。
GC“回调/通知”业务线程
Reference
存在一个静态代码块会启动一个线程,私有静态成员 pending
由 垃圾收集器设置,并唤醒这个线程处理相关逻辑。摘要原代码:
public abstract class Reference<T> {
//由 collector 设置,并唤醒下面启动的线程
private static Reference<Object> pending = null;
static {
...
//处理逻辑,如果 pending == null 则 wait
//否则为clearner对象,则直接调用clear() clear方法一般会开启线程,不应该阻塞这个loop
//否则存在ReferenceQueue,则放入 queue 中并唤醒等待的用户线程
Thread handler = new ReferenceHandler(tg, "Reference Handler");
//最高优先级
handler.setPriority(Thread.MAX_PRIORITY);
//守护线程
handler.setDaemon(true);
handler.start();
...
}
}
ThreadLocal & WeakReference 的使用
ThreadLocal
本质上是一个门面类。通过它设置value,本质上是,在 Thread
的成员变量 ThreadLocal.ThreadLocalMap threadLocals = null;
中放入 Entry<k,v>
,其中 k 为这个 ThreadLocal
对象,value 为需要存的数值。这样的话就会有内存泄漏的风险,代码描述如下:
//产生一个threadLocal对象
ThreadLocal<Object> threadLocal = new ThreadLocal();
...
//产生一个Thread t
new Thread(()->{
//某些场景下设置 ThreadLocal 变量
//本质是在 threadLocalMap 中放入一个 Entry<threadLocal,Object>
threadLocal.set(new Object());
while (true) {
//之后去使用这个内容
threadLocal.get();
}
}).start();
...
//在之后的某块代码,将这个ThreadLocal给设置成null了
//之后,线程内通过这个 ThreadLocal 其实已经无法访问到期望的 value 了
//但实际上,Entry<threadLocal,Object> 仍然被 threadLocalMap 强引用,占用着内存
threadLocal = null;
从开发角度来说,将 threadLocal = null;
=> threadLocal.remove()
就可以解决这个问题。从Java语言层面其实ThreadLocal
机制也存在其他操作来减少内存溢出的风险。
看一下ThreadLocalMap的实现
static class ThreadLocalMap {
//继承了 WeakRefernce ,Entry的key其实时一个弱饮用
//也就是说,当ThreadLocal没有任何强引用的时候,通过 Reference#get()方法获取key就会是 null
static class Entry extends WeakReference<ThreadLocal<?>> {
Entry(ThreadLocal<?> k, Object v) {
//k == ThreadLocal
//Entry 的 Key 时 WeakReference,当没有强引用时,会get到null
super(k);
value = v;
}
}
//这个方法内,会移除掉 key == null 的 Entry
//这个方法会在,put 的时候,如果 key 为 null 时调用
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
}
}
简单来说就是,ThreadLocalMap 中 Entry<K,V> K是一个 ThreadLocal 的弱引用,当 ThreadLocal 没有任何强引用时,Entry的再获取 K的时候,会得到一个 null,再下一次 put 的时候,就会从 ThreadLocalMap 中溢出掉所有 key 为 null 的 Entry。
DirectByteBuffer & PhantomReference 的使用
//设置堆最大最小10m,直接内存最大使用10m
//-Xmx10m -Xms10m -XX:MaxDirectMemorySize=10m
public static void main(String[] args) throws IOException {
//分配10m directbuffer
ByteBuffer buff1 = ByteBuffer.allocateDirect(1024 * 1024 * 10);
//GC之后,在分配10m,这里并不会内存溢出
buff1 = null;
//这个显示调用去掉,其实在直接内存不足的时候,也会自动出发 FullGC
System.gc();
ByteBuffer buff2 = ByteBuffer.allocateDirect(1024 * 1024 * 10);
}
看上面的代码举例,很奇怪的一点是,GC 一般来说只会对 Java堆 以及 MateSpace(1.8 方法区实现)做回收,那为什么直接内存在运行了 System.gc()
之后也仿佛被回收了呢?
DirectByteBuffer 时怎么被释放的呢?
答案在于 DirectByteBuffer
的创建过程,代码如下:
DirectByteBuffer(int cap) { // package-private
...
//关键在于这个 Cleaner,对于 DirectByteBuffer 的虚引用,并且接受一个 Runnable,这里是 Deallocator。
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
这个Cleaner其实本质上是一个对 DirectByteBuffer
对象的虚引用,并且还接受了一个 Deallocator
对象(本质上时一个Runnable,核心代码是通过unsafe释放内存)。上面讲过,再 GC 时,收集器一旦发现一个引用可达发生了变化,就会走 GC“回调/通知”业务线程 这一套逻辑(上面讲过了)。从而调用了 Cleaner#clean
,在这个例子中,这个方法,其实就是运行 Dealocator
,最终通过 unsafe.freeMemory(address)
释放内存。
总的来说就是,通过Reference
(具体来说是PhantomPeference
)的通知/回调机制,在回收引用对象时,运行一段用户代码,调用unsafe.freeMemory(address)
释放了直接内存。
除了 ThreadLocal
DirectByteBuffer
外,其他利用 Reference
在垃圾回收时,触发一些用户操作的类,还有很多。如:WeakHashMap
利用 WeakReference
防止内存溢出。
上述中,我这里将 Reference
这个机制,叫做 GC“回调/通知”业务线程 并不妥当,原谅我已经词穷了,介于这个词语可以直观的反馈这个机制,还请大家见谅。