以 ZGC 为例,谈一谈 JVM 是怎样实现 Reference 语义的

络腮胡菲菲  金牌会员 | 2024-6-13 10:47:15 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 903|帖子 903|积分 2709

本文基于 OpenJDK17 进行讨论

1. Reference 相关概念及其应用场景总览

Reference(引用)是 JVM 中非常焦点且重要的一个概念,垃圾回收器判断一个对象存活与否都是围绕着这个 Reference 来的,JVM 将 Reference 又细分为几种具体的引用范例,它们分别是:StrongReference,SoftReference,WeakReference,PhantomReference,FinalReference。
谈到这些 Reference,可谓是既熟悉又陌生,因此笔者在本文中先容 Reference 的思路也是从它熟悉的一面再到陌生的一面进行展开讨论。
我们在 JDK 以及一些中间件源码中或多或少见过他们的身影,对他们的应用场景以及概念非常熟悉。比如:
1.1 StrongReference

StrongReference:强引用关系不用多说,这个我们最熟悉了,大部门 Java 对象之间的关系都是强引用,只要对象与 GcRoot 之间有强引用关系的存在,那么这个对象将永远不会被垃圾回收器回收。
  1. Object gcRoot = new Object();
复制代码
1.2 SoftReference

SoftReference:如果对象只有一条软引用关联,那么当内存充足的时间,软引用和强引用一样,在发生 GC 的时间,只被软引用关联的对象是不会被回收掉的。当内存不敷的时间也就是系统将要发生 OOM 之前,此时发生 GC,那么这些只被软引用所关联的对象将会被当做垃圾回收掉。
  1. SoftReference gcRoot = new SoftReference<Object>(new Object());
复制代码
上面这行代码展示的引用关系如下图所示:

gcRoot 强引用了 SoftReference 对象 ,然后 SoftReference 对象软引用了 Object 对象,那么此时对于 Object 对象来说就只存在一条软引用关系 —— SoftReference对象 -> Object对象,当系统将要发生 OOM 之前,GC 就会将 Object 对象回收掉。背面我们通过 SoftReference#get 方法获取到的引用对象将会是 Null (Object 对象已被回收)。
根据 SoftReference 的这个特性,我们可以用它来引用持有一些 memory-sensitive caches 等有用但是非必须的对象。比如,Guava 中的 CacheBuilder.softValues() 就可以让 cache 使用 SoftReference 来引用持有 Values,当内存不敷的时间回收掉就好了。
  1. Cache<Object, Object> softCache = CacheBuilder.newBuilder().softValues().build();
复制代码
还有在池化技术的实现中,比如对象池,毗连池这些,我们也可以在池中用 SoftReference 来引用持有被池化的这些对象。比如一些 RPC 框架中比方 Dubbo,在从 Sokcet 中读取到网络传输进来的二进制数据时,需要将这些网络二进制数据序列化成 Java 类,方便后续业务逻辑的处理。
当我们接纳 Kryo 序列化框架时,每一次的序列化都需要用到一个叫做 Kryo 类的实例,由于 Kryo 并不是线程安全的,再加上创建初始化一个 Kryo 实例代价比较高,所以在多线程环境中,我们需要使用 ThreadLocal 来持有  Kryo 实例或者使用 KryoPool 对象池来池化 Kryo 实例。
由于这里我们先容的是 SoftReference 的使用场景,所以我们以 KryoPool 为例说明,在 KryoPool 的创建过程中,我们可以指定使用 softReferences 来持有这些 Kryo 实例,当内存不敷的时间,GC 将会回收这些 Kryo 实例。
  1. public class PooledKryoFactory extends AbstractKryoFactory {
  2.     private KryoPool pool;
  3.     public PooledKryoFactory() {
  4.         pool = new KryoPool.Builder(this).softReferences().build();
  5.     }
  6.     @Override
  7.     public Kryo getKryo() {
  8.         return pool.borrow();
  9.     }
  10.     @Override
  11.     public void returnKryo(Kryo kryo) {
  12.         pool.release(kryo);
  13.     }
  14. }
复制代码
后续多线程在遇到序列化任务时,直接从 KryoPool 中去获取  Kryo 实例,序列化完成之后再将  Kryo 实例归还到池中。当然了,Dubbo 使用的是另一种方式,通过 ThreadLocal 来持有 Kryo 实例也可以达到同样的目的。这里笔者就不深入写了。
  1. public class ThreadLocalKryoFactory extends AbstractKryoFactory {
  2.     private final ThreadLocal<Kryo> holder = new ThreadLocal<Kryo>() {
  3.         @Override
  4.         protected Kryo initialValue() {
  5.             return create();
  6.         }
  7.     };
  8.     @Override
  9.     public void returnKryo(Kryo kryo) {
  10.         // do nothing
  11.     }
  12.     @Override
  13.     public Kryo getKryo() {
  14.         return holder.get();
  15.     }
  16. }
复制代码
1.3 WeakReference

WeakReference:弱引用是比 SoftReference 更弱的一种引用关系,如果被引用对象当前只存在一条弱引用链时,那么发生 GC  的时间,无论内存是否足够,只被弱引用所关联的对象都会被回收掉。
  1. WeakReference gcRoot = new WeakReference<Object>(new Object());
复制代码
上面这行代码展示的引用关系如下图所示:

gcRoot 强引用了 WeakReference 对象 ,然后 WeakReference 对象弱引用了 Object 对象,那么此时对于 Object 对象来说就只存在一条弱引用关系 —— WeakReference对象 -> Object对象。当发生 GC 时, Object 对象就会被回收掉。背面我们通过 WeakReference#get 方法获取到的引用对象将会是 Null (Object 对象已被回收)。
和 SoftReference 一样,WeakReference 也经常被用在缓存框架以及池化技术中,只不过引用强度更弱一些。比如,在 Guava 中,我们可以通过 CacheBuilder.weakKeys()来指定由弱引用来持有缓存 Key,当系统中只有一条弱引用(缓存框架)来持有缓存 Key ,除此之外没有任何的强引用或者软引用持有 Key 时,那么在 GC 的时间,缓存 Key 就会被回收掉,随后 Guava 也会将这个 Key 对应的整个 entry 清理掉。
同样我们也可以通过 CacheBuilder.weakValues() 来指定由弱引用来持有缓存 Value,当系统中没有任何强引用或者软引用来持有缓存 Value 时,发生 GC 的时间,缓存的这条 entry 也是会被回收掉的。
  1. Cache<Object,Object> weakCache = CacheBuilder.newBuilder().weakKeys().weakValues().build();
复制代码
WeakReference 在池化技术中运用的更加广泛些,比如,在 Netty 对象池 Recycler 的设计中:

每个线程都会拥有一个属于自己的 Stack,Stack 中包含一个用数组实现的栈结构(图中绿色部门),这个栈结构正是对象池中真正用于存储池化对象的地方,我们每次从对象池中获取对象都会从这个栈结构中弹出栈顶元素。同样我们每次将使用完的对象归还到对象池中也是将对象压入这个栈结构中
每个线程拥有一个独立 Stack,如许当多个线程并发从对象池中获取对象时,都是从自己线程中的 Stack 中获取,全程无锁化运行。大大进步了多线程从对象池中获取对象的效率。
线程与 Stack 是一对一的结构,我们看到在 Stack 结构中通过 WeakReference 持有了 Thread 的引用。
  1. private static final class Stack<T> {
  2.         final WeakReference<Thread> threadRef;
  3. }
复制代码
这是因为对象池的设计中存在如许一个引用关系:池化对象 -> DefaultHandler(池化对象在池中的模型) -> stack -> threadRef。而池化对象是暴露给用户的,如果用户在某个地方持有了池化对象的强引用忘记清理,而 Stack 持有 Thread 的强引用的话,当线程挂掉的之后,因为如许一个强引用链的存在从而导致 Thread 不绝不能被 GC 回收。
别的,为了使多线程回收对象的时间也能够无锁化的进行,每一个回收线程都对应一个 WeakOrderQueue 节点(上图黄色部门),回收线程会将池化对象暂时回收到自己对应的 WeakOrderQueue 结构中,当对象池中的 Stack 没有对象时,就会由  Stack 弱引用关联的 Thread 将 WeakOrderQueue 结构中暂存的回收对象转移到 Stack 中。对象的申请只能从 Stack 中进行,整个申请,回收过程都是无锁化进行的。
WeakOrderQueue 结构也是由 WeakReference 实现的,由于 WeakOrderQueue 与回收线程也是一对一的关系,所以在 WeakOrderQueue 中也是通过弱引用来持有回收线程的实例。
  1. private static final class WeakOrderQueue extends WeakReference<Thread> {
  2. }
复制代码
目的也是为了当回收线程挂掉的时间,能够包管回收线程被 GC 及时的回收掉。如果 WeakOrderQueue.get() == null 说明当前 WeakOrderQueue 节点对应的回收线程已经挂掉了,此时如果当前节点还有待回收对象,则需要将节点中的所有待回收对象全部转移至 Stack 的数组栈中。
转移完成之后,将该 WeakOrderQueue 节点从 Stack 结构里的 WeakOrderQueue 链表中删除。包管被清理后的 WeakOrderQueue 节点可以被 GC 回收。
关于 Recycler 对象池相关的实现细节,感兴趣的同学可以回看下 《详解Recycler对象池的精妙设计与实现》
在比如,Netty 中的资源泄露探测工具 ResourceLeakDetector 也是通过 WeakReference  来探测资源是否存在泄露的,默认是开启的,但我们也可以通过 -Dio.netty.leakDetection.level=DISABLED 来关闭资源泄露探测。
Netty 中的 ByteBuf 是一种内存资源,我们可以通过 ResourceLeakDetector 来探测我们的工程是否存在内存泄露的状况,这里面有一个非常重要的类 DefaultResourceLeak 就是一个弱引用 WeakReference。由它来弱引用 ByteBuf。
  1. private static final class DefaultResourceLeak<T> extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
  2.     private final Set<DefaultResourceLeak<?>> allLeaks;
  3.     DefaultResourceLeak(
  4.                 Object referent,
  5.                 ReferenceQueue<Object> refQueue,
  6.                 Set<DefaultResourceLeak<?>> allLeaks) {
  7.             // 弱引用 ByteBuf
  8.             super(referent, refQueue);   
  9.             // 将弱引用 DefaultResourceLeak 放入全局 allLeaks 集合中
  10.             allLeaks.add(this);
  11.             this.allLeaks = allLeaks;
  12.     }
  13. }
复制代码
在每创建一个 ByteBuf 的时间, Netty 都会创建一个 DefaultResourceLeak 实例来弱引用 ByteBuf,并且会将这个 DefaultResourceLeak 实例放入到一个全局的 allLeaks 集合中。Netty 中的每个 ByteBuf 都会有一个 refCnt 来表示对这块内存的引用情况。
  1. public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
  2.    // 引用计数
  3.    private volatile int refCnt = updater.initialValue();
  4. }
复制代码
对于 ByteBuf 的每一次引用 —— ByteBuf.retain(),都会增加一次引用计数 refCnt。对于 ByteBuf 的每一次释放 —— ByteBuf.release(),都会减少一次引用计数 refCnt。当引用计数 refCnt 为 0 时,Netty 就会将与 ByteBuf 弱引用关联的 DefaultResourceLeak 实例从 allLeaks 中删除。
由于 DefaultResourceLeak 只是用来追踪 ByteBuf 的资源泄露情况,它并不能影响 ByteBuf 是否存活,所以 Netty 这里只是让  DefaultResourceLeak 来弱引用一下 ByteBuf。当 ByteBuf 在系统中没有任何强引用或者软引用时,那么就只有一个 DefaultResourceLeak 实例在弱引用它了,发生 GC 的时间 ByteBuf 就会被回收掉。
Netty 判断是否发生内存泄露的时机就发生在 ByteBuf 被 GC 的时间,这时 Netty 会拿到被 GC 掉的 ByteBuf 对应的弱引用 DefaultResourceLeak 实例,然后查抄它的 allLeaks 集合是否仍然包含这个 DefaultResourceLeak 实例,如果包含就说明 ByteBuf 有内存泄露的情况。
因为如果 ByteBuf 的引用计数 refCnt 为 0 时,Netty 就会将弱引用 ByteBuf 的 DefaultResourceLeak 实例从 allLeaks 中删除。ByteBuf 现在都被 GC 了,它的 DefaultResourceLeak 实比方果还存在 allLeaks 中,那说明我们就根本没有调用 ByteBuf.release() 去释放内存资源。
在探测到内存泄露发生之后,后续 Netty 就会通过 reportLeak() 将内存泄露的相关信息以 error 的日志级别输出到日志中。
除此之外,WeakReference 的应用场景还有许多,比如在无锁化的设计中频仍用到的 ThreadLocal :
  1.    ThreadLocal<Object> gcRoot = new ThreadLocal<Object>(){
  2.             @Override
  3.             protected Object initialValue() {
  4.                 return new Object();
  5.             }
  6.    };
复制代码
ThreadLocal 顾名思义是线程本地变量,当我们在程序中定义了一个 ThreadLocal 对象之后,那么在多线程环境中,每个线程都会拥有一个独立的 ThreadLocal 对象副本,这就使得多线程可以独立的操纵这个 ThreadLocal 变量不需要加锁。
为了完成线程本地变量的语义,JDK 在 Thread 中添加了一个 ThreadLocalMap 对象,用来持有属于自己本地的 ThreadLocal 变量副本。
  1. public class Thread implements Runnable {
  2.         ThreadLocal.ThreadLocalMap threadLocals = null;
  3. }
复制代码
由于我们通常在程序中会定义多个 ThreadLocal 变量,所以 ThreadLocalMap 被设计成了一个哈希表的结构 —— Entry[] table,多个 ThreadLocal 变量的本地副本就保存在这个 table 中。
  1. static class ThreadLocalMap {
  2.         private Entry[] table;
  3. }
复制代码
table 中的每一个元素是一个 Entry 结构,Entry 被设计成了一个 WeakReference,由 Entry 来弱引用持有 ThreadLocal 对象(作为 key), 强引用持有 value 。如许一来,ThreadLocal 对象和它所对应的 value 就被 Entry 关联起来了。
  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2.         Object value;
  3.         Entry(ThreadLocal<?> k, Object v) {      
  4.                 // 弱引用     
  5.                 super(k);
  6.                 // 强引用
  7.                 value = v;
  8.         }
  9. }
复制代码
当某一个线程开始调用 ThreadLocal 对象的 get 方法时:
  1.         ThreadLocal<Object> gcRoot = new ThreadLocal<Object>(){
  2.             @Override
  3.             protected Object initialValue() {
  4.                 return new Object();
  5.             }
  6.         };
  7.         gcRoot.get();
复制代码
JDK 首先会找到本地线程中保存的 ThreadLocal 变量副本 —— ThreadLocalMap,然后以 ThreadLocal 对象为 key  —— 也就是上面 gcRoot  变量引用的 ThreadLocal 对象,到哈希表 table 中查找对应的 Entry 结构(WeakReference),近而通过 Entry. value  找到该 ThreadLocal 对象对应的 value 值返回。
  1. public class ThreadLocal<T> {
  2.     public T get() {
  3.         Thread t = Thread.currentThread();
  4.         // 获取本地线程中存储的 ThreadLocal 变量副本
  5.         ThreadLocalMap map = getMap(t);
  6.         if (map != null) {
  7.             // 以 ThreadLocal 对象为 key,到哈希表 table 中查找对应的 Entry 结构
  8.             ThreadLocalMap.Entry e = map.getEntry(this);
  9.             if (e != null) {
  10.                 // 返回该 threadLocal 对象对应的 value。
  11.                 T result = (T)e.value;
  12.                 return result;
  13.             }
  14.         }
  15.         // 如果 threadLocal 对象还未设置 value 值的话,则调用 initialValue 初始化 threadLocal 对象的值
  16.         return setInitialValue();
  17.     }
  18. }
复制代码
以上面这段示例代码为例,当前系统中的这个 ThreadLocal 对象 —— 也就是由 gcRoot  变量指向的 ThreadLocal 对象,存在以下两条引用链:


  • 一条是 Thead对象 -> ThreadLocalMap对象->Entry对象 这条弱引用链。
  • 另一条则是有 gcRoot变量 -> ThreadLocal对象 这条强引用链。
当我们通过 gcRoot = null 来断开 gcRoot 变量到 ThreadLocal 对象的强引用之后,ThreadLocal 对象在系统中就只剩下一条弱引用链存在了。

Entry 被设计成一个 WeakReference,由它来弱引用 ThreadLocal 对象的好处就是,当系统中不存在任何对这个 ThreadLocal 对象的强引用之后,发生 GC 的时间这个 ThreadLocal 对象就会被回收掉。后续我们在通过 Entry.get() 获取 Key(ThreadLocal 对象)的时间就会得到一个 Null 。
固然现在 ThreadLocal 对象已经被 GC 掉了,但 JDK  对于 Reference 的处理流程还没有竣事,究竟上对于 Reference 的处理是需要 GC 线程以及 Java 业务线程相互配合完成的,这也是本文我们要重点讨论的主题。(絮聒了这么久,终于要引入主题了)
GC 线程负责回收被 WeakReference 引用的对象,也就是这里的 ThreadLocal 对象。但别忘了这里的 Entry 对象本身也是一个 WeakReference 范例的对象。被它弱引用的对象现在已经回收掉了,那么与其关联的 Entry 对象以及 value 其实也没啥用处了。
但如上图所示,Entry 对象以及 value 对象却还是存在一条强引用链,固然他们没什么用了,但仍然无法被回收,如果 Java 业务线程不做任何处理的话就会导致内存泄露。
在 ThreadLocal 的设计中 ,Java 业务线程清理无用 Entry 的时机有以下三种:

  • 当我们在线程中通过 ThreadLocal.get() 获取恣意 ThreadLocal 变量值的时间,如果发生哈希辩论,随后接纳开放定址办理辩论的过程中,如果发现 key 为 null 的 Entry,那么就将该 Entry以及与其关联的 vakue 设置为 null。末了以此 Entry 对象为起点遍历整个 ThreadLocalMap 清理所有无用的 Entry 对象。但这里需要留意的是如果 ThreadLocal.get() 没有发生哈希辩论(直接掷中),或者在办理哈希辩论的过程中没有发现 key 为 null 的 Entry,那么就不会触发无用 Entry 的清理,仍然存在内存泄露的风险。
  1.        private int expungeStaleEntry(int staleSlot) {
  2.             Entry[] tab = table;
  3.             int len = tab.length;
  4.             // 将当前遍历到的 Entry 以及与其关联的 value 设置为 null
  5.             tab[staleSlot].value = null;
  6.             tab[staleSlot] = null;
  7.             size--;
  8.    
  9.             Entry e;
  10.             int i;
  11.             // 遍历整个 ThreadLocalMap,清理无用的 Entry
  12.             for (i = nextIndex(staleSlot, len);
  13.                  (e = tab[i]) != null;
  14.                  i = nextIndex(i, len)) {
  15.                 ThreadLocal<?> k = e.get();
  16.                 if (k == null) {
  17.                     e.value = null;
  18.                     tab[i] = null;
  19.                     size--;
  20.                 }
  21.             }
  22.             return i;
  23.         }
复制代码

  • 当我们在线程中通过 ThreadLocal.set(value) 设置恣意 ThreadLocal 变量值的时间,如果直接通过 ThreadLocal 变量定位到了 Entry 的位置,那么直接设置 value 返回,并不会触发无用 Entry 的清理。如果在定位 Entry 的时间发生哈希辩论,随后会通过开放定址在 ThreadLocalMap 中探求到一个合适的 Entry 位置。并从这个位置开始向后扫描 log2(size) 个 Entry,如果在扫描的过程中发现有一个是无用的 Entry,那么就会遍历整个 ThreadLocalMap 清理所有无用的 Entry 对象。但如果恰好这 log2(size) 个 Entry 都是有用的,即使背面存在无用的 Entry 也不会再清理了,这也导致了内存泄露的风险。
  1.         // 参数 i 表示开发定址定位到的 Entry 位置
  2.         // n 为当前 ThreadLocalMap 的 size
  3.         private boolean cleanSomeSlots(int i, int n) {
  4.             boolean removed = false;
  5.             Entry[] tab = table;
  6.             int len = tab.length;
  7.             do {
  8.                 i = nextIndex(i, len);
  9.                 Entry e = tab[i];
  10.                 // 如果发现有一个是无用 Entry
  11.                 if (e != null && e.refersTo(null)) {
  12.                     // 遍历整个 ThreadLocalMap 清理所有无用的 Entry 对象
  13.                     i = expungeStaleEntry(i);
  14.                 }
  15.             } while ( (n >>>= 1) != 0); // 向后扫描 log2(size) 个 Entry
  16.             return removed;
  17.         }
复制代码

  • 前面先容的 get , set 方法只是顺手清理一下 ThreadLocalMap 中无用的 Entry,但并不一定包管能够触发到清理动作,所以仍然面临内存泄露的风险。一个更加安全有用的方式是我们需要在使用完 ThreadLocal 对象的时间,手动调用它的 remove 方法,及时清理掉 Entry 对象并通过 Entry.clear() 断开 Entry  到 ThreadLocal 对象之间的弱引用关系,如许一来,当 ThreadLocal 对象被 GC 的时间,与它相关的 Entry 对象以及 value 也会被一并 GC ,如许就彻底杜绝了内存泄露的风险。
  1.         private void remove(ThreadLocal<?> key) {
  2.             Entry[] tab = table;
  3.             int len = tab.length;
  4.             // 确定 key 在 table 中的起始位置
  5.             int i = key.threadLocalHashCode & (len-1);
  6.             for (Entry e = tab[i];
  7.                  e != null;
  8.                  e = tab[i = nextIndex(i, len)]) {
  9.                 if (e.refersTo(key)) {
  10.                     // 断开 Entry  到 ThreadLocal 对象之间的弱引用关系
  11.                     e.clear();
  12.                     // 清理 key 为 null 的 Entry 对象。
  13.                     expungeStaleEntry(i);
  14.                     return;
  15.                 }
  16.             }
  17.         }
复制代码
1.4 PhantomReference

PhantomReference:虚引用其实和 WeakReference 差不多,他们共同的特点都是一旦发生 GC,PhantomReference 和 WeakReference 所引用的对象都会被 GC 掉,当然前提是这个对象没有其他的强引用或者软引用存在。差别的是 PhantomReference 无法像其他引用范例一样能够通过 get 方法获取到被引用的对象。
  1. public class PhantomReference<T> extends Reference<T> {
  2.     public T get() {
  3.         return null;
  4.     }
  5. }
复制代码
看上去这个 PhantomReference 似乎是没啥用处,因为它既不能影响被引用对象的生命周期,也无法通过它来访问被引用对象。但其实否则,我们用 PhantomReference 来虚引用一个对象的目的其实为了能够在这个对象被 GC 之后,我们在 Java 业务线程中能够感知到,在感知到对象被 GC 之后,可以使用这个 PhantomReference 做一些资源清理的动作。
比如与 DirectByteBuffer 关联的 Cleaner 对象,其实就是一个 PhantomReference 的实现,当 DirectByteBuffer 被 GC 掉之后,由于其与 Cleaner 虚引用关联,所以我们可以在 Java 业务线程中感知到,随后可以使用 Cleaner 来释放 DirectByteBuffer 背后引用的 Native Memory。
1.5 FinalReference

FinalReference 只是和 Java 类中的 finalize 方法(OpenJDK9 中已被废弃)的执行机制有关,但笔者建议大家忘记 JDK 中还有这么个玩意存在,因为 FinalReference 会使 Java 对象的回收过程变得磨磨唧唧,拖拖沓拉。可能需要等上好几轮 GC,对象才可以被回收掉。所以我们基本不访问到  FinalReference 的应用场景,因为这玩意磨磨唧唧的没啥卵用。不过笔者还是会在本文的末了先容它,目的不是为了让大家使用它,而是在末尾使用它再次给大家串联一下 JVM 关于 Reference 的整个处理流程。
2.  Reference —— 最熟悉的陌生人

以上先容的都是我们熟悉的 Reference,之所以说它熟悉,是因为上述 Reference 相关的处理逻辑都发生在 Java 的业务线程中,如果我们站在 JVM 全局视角上看,Reference 的完整处理流程是需要 Java 线程和 GC 线程一起相互配合完成的。
之所以说它陌生,是因为 GC 线程处理的过程对我们来说还是一个黑盒,这使得我们无法触达 Reference 的本质,如果我们只是从概念上去理解 Reference 的话,一定会有许多模棱两可,似是而非的感觉,信赖大家在阅读第一末节的过程中都会伴随着这种感觉,比如:

  • 当内存不敷的时间,那些只被 SoftReference 引用的对象将被 GC 回收掉,那么这个 “内存不敷” 的概念就很模糊,怎样定义内存不敷 ?能不能量化出一个具体的回收时机 ?到底什么情况下会触发回收 ?
  • 当发生 GC 的时间,无论内存是否足够,那些只被 WeakReference 所引用的对象都会被回收掉。但大家别忘了这些 WeakReference 本身其实也是一个平凡的 Java 对象,GC 在判断一个对象是否存活的依据,就是沿着 GcRoot 的引用关系链开始逐个遍历,遍历到的对象就标记为存活,没有遍历到的对象就不标记。后续将被标记的对象统一转移到新的内存区域,然后一股脑的扫除没有被标记的对象。如果一个 GcRoot 引用了 WeakReference 对象。而 WeakReference 对象又引用了一个 Object 对象。那么按道理来说这个 Object 对象也在引用链上啊,应该也是能够被 GC 标记到的啊,那为什么就被 GC 给回收了呢 ? JVM 到底是怎样实现 WeakReference 弱引用语义的呢 ?
  • PhantomReference 既不能影响被引用对象的生命周期,也无法通过它来访问被引用对象,那我们要它干嘛 ? 用 PhantomReference 来虚引用一个对象的目的就是为了跟踪被引用对象是否被 GC 回收了,当虚引用的对象被 GC 之后,我们在 Java 线程中可以感知到,近而做一些相关资源清理的动作。那么请问 Java 线程是怎样感知到的 ? GC  线程怎样通知 ? Java 线程怎样收到这个通知,它们两者是怎样配合的 ?
  • FinalReference 到底是怎样让 Java 对象的回收过程变得磨磨唧唧,拖拖沓拉的呢 ?
  • PhantomReference 和 WeakReference 看起来似乎都差不多,如果 PhantomReference 能够跟踪对象的回收状态,那么 WeakReference 可不可以 ?它们之间究竟有何差别 ?
  • 上面聊到的这些 Reference 范例:SoftReference,WeakReference,PhantomReference,FinalReference 其实本质上都是一个平凡的 Java 类,由他们定义的对象也只是一个平凡的 Java 对象。而当这些 Reference 对象引用的 Object 对象被 GC 回收掉之后,其实这些 Reference 对象也就没用了,但由于 GcRoot 还能关联到这些 Reference 对象,从而导致这些无用的 Reference 对象无法被 GC 回收。那么我们在 Java 业务线程中该怎样处理这些无用的 Reference 对象 ?这些 Reference 对象又是在什么时间被回收的呢 ?

笔者将会在本文中为大家解答上述这六个问题,如果屏幕前的你是一个 Java 小白,那么恭喜你,接下来你可以无障碍地阅读本文的内容,因为你心中没有杂念,在探寻 Reference 本质的过程中不会被既定的思维模式所影响和误导。你只需要始终牢记一点,就是这些 Reference 只是一个平凡的 Java 类,它们的对象也只是一个平凡的 Java 对象。 笔者不绝夸大的这个概念看起来很傻,但是真的非常重要,因为它是我们探寻 Reference 本质的起点。
  1. public abstract class Reference<T> {
  2. }
  3. public class SoftReference<T> extends Reference<T> {
  4. }
  5. public class WeakReference<T> extends Reference<T> {
  6. }
  7. public class PhantomReference<T> extends Reference<T> {
  8. }
复制代码
如果屏幕前的你是一个 Java 老司机,那么请你!现在!立刻!马上!忘记所有关于 Reference 的既有概念和印象。就是把它当做一个平凡的 Java 类来对待,重新以这个为起点,跟随笔者的思路一步一步探寻 Reference 的本质。

下面我们就从一个具体的问题开始,来正式开启本文的内容~~~
3. JVM 怎样回收 DirectBuffer 背后的 NativeMemory

NIO 中的 DirectByteBuffer 究其本质而言,其实只是 OS 中的 Native Memory 在 JVM 中的一种封装形式,DirectByteBuffer 本身非常具有疑惑性,因为 JVM 能够察觉到的只是 DirectByteBuffer 这个 Java 实例在 JVM 堆中占用的那么一点点的内存,而 DirectByteBuffer 背后所引用的大片 OS 中的 Native Memory,JVM 是察觉不到的。
所以人们将 DirectByteBuffer 实例形象的比喻为 “冰山对象”,DirectByteBuffer 实例就是冰山上的那一角,位于 JVM 堆中,内存占用非常小,它宁静凡的 Java 对象一样,当没有任何强引用或者软引用的时间,将会被 GC 回收掉。

但位于冰山下面的这一大片 Native Memory,GC 是管不到的。也就是说 GC 只能回收 DirectByteBuffer 实例占用的这一小部门 JVM 堆内存,但 GC 无法回收 DirectByteBuffer 背后引用的这一大片 Native Memory。
岂非就只能让 JVM 进程拿着这一大片 Native Memory 不放直到 OOM 吗?如许肯定是不行的,那 JVM 是怎样回收  DirectByteBuffer 背后引用的这片 Native Memory 呢 ?接下来我们能不能试着看从 DirectByteBuffer 的创建过程中找到一些蛛丝马迹。
  1. class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
  2. {
  3.     private final Cleaner cleaner;
  4.     DirectByteBuffer(int cap) {                   // package-private
  5.         ...... 省略 .....   
  6.         // 检查堆外内存整体用量是否超过了 -XX:MaxDirectMemorySize
  7.         // 如果超过则尝试等待一下 JVM 回收堆外内存,回收之后还不够的话则抛出 OutOfMemoryError
  8.         Bits.reserveMemory(size, cap);   
  9.         // 底层调用 malloc 申请虚拟内存
  10.         base = UNSAFE.allocateMemory(size);
  11.         ...... 省略 .....   
  12.         cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
  13.     }
  14. }
复制代码
DirectByteBuffer 的创建主要由下面的三大焦点步骤来完成,JDK 会首先通过 Bits.reserveMemory 来查抄当前 JVM 进程的堆外内存用量是否超过了 -XX:MaxDirectMemorySize 指定的最大堆外内存限定。
如果已经超过了,则会在这里进行一下补救,查抄一下当前是否有其他的 DirectByteBuffer 被 GC 掉,如果有则等待一下 JDK 去释放这些被引用的 native memory。
如果释放之后堆外内存容量还是不够,那么就触发 System.gc() 尝试看能不能再只管回收一些没用的 DirectByteBuffer,如果又回收了一些 DirectByteBuffer,则再次等待一下 JDK 去释放这些被引用的 native memory。在这一系列的补救步调施行完之后如果堆外内存容量还是不够,则触发 OOM。
这里只需要了解一下 Bits.reserveMemory 的焦点逻辑即可,相关的 native memory 回收细节可巧是本末节的主题,笔者背面会对这些细节进行先容。
如果堆外内存容量足够,则通过 UNSAFE.allocateMemory 向 OS 申请 native memory 。
末了一步非常关键,我们看到 DirectByteBuffer 内部关联了一个 Cleaner 对象。这岂非是巧合吗 ?我们可巧正在讨论  native memory 回收的场景,从  Cleaner 的命名上来推测,顾名思义,这里岂非就是释放 native memory 的地方吗 ?
下面我们来通过 Cleaner.create 方法进入到 Cleaner 的内部试着看探求一下答案:
3.1 Cleaner 机制的设计与实现
  1. public class Cleaner extends PhantomReference<Object>
  2. {      
  3.         public static Cleaner create(Object ob, Runnable thunk) {
  4.               if (thunk == null)
  5.                   return null;
  6.               return add(new Cleaner(ob, thunk));
  7.         }   
  8.         // Deallocator
  9.         private final Runnable thunk;
  10.         // 忽略这个 dummyQueue,Cleaner 并不会用到它
  11.         private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
  12.         private Cleaner(Object referent, Runnable thunk) {
  13.             // 调用 PhantomReference 类的构造函数
  14.             super(referent, dummyQueue);
  15.             // Deallocator
  16.             this.thunk = thunk;
  17.         }
  18. }
复制代码
关键的信息出现了,Cleaner 类原来继承自 PhantomReference 范例,这里要创建的 Cleaner 对象本质上其实就是一个 PhantomReference 对象。
JDK  首先会通过 Cleaner 类的私有构造方法构造出一个 PhantomReference 对象。构建参数 referent 就是我们创建好的 DirectByteBuffer,thunk 就是在 DirectByteBuffer 构造函数中传入的 Deallocator,Deallocator 我们先不用管,这里大家只需要记得 Cleaner 类中的 thunk 字段其实指向的是 Deallocator 对象就可以了。
在 Cleaner 的父类构造函数构建 PhantomReference 对象的时间又传入了一个 ReferenceQueue 范例的 dummyQueue,这个 ReferenceQueue 在 Reference 机制中是一个非常重要的概念,但本末节中我们不会用到它,不需要管。但大家需要记着这个 ReferenceQueue 很重要,笔者背面还会再次提起。
Reference 类中有一个很重要的字段 referent,用来关联 Reference 所引用的对象,这里 referent 指向的是 DirectByteBuffer 对象。这里大家先不用管什么虚引用,弱引用,软引用的语义,只需要知道 PhantomReference 就是一个平凡的对象,对象中有一个平凡的字段 referent 现在指向了 DirectByteBuffer 对象。
  1. public abstract class Reference<T> {
  2.     // PhantomReference 虚引用的对象
  3.     private T referent;
  4.     // 暂时先不要管这个 ReferenceQueue,但它是非常重要的一个概念
  5.     volatile ReferenceQueue<? super T> queue;
  6.     Reference(T referent, ReferenceQueue<? super T> queue) {
  7.         //  PhantomReference 虚引用了 DirectByteBuffer 对象
  8.         this.referent = referent;
  9.         this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
  10.     }
  11. }
复制代码
当一个 Reference 对象被 JVM 放入 _reference_pending_list 链表之后,JVM 就会将 Reference 对象中的 referent 字段设置为 null,扫除它的引用关系(虚引用 or 弱引用 or 软引用)。
  1. public class Cleaner extends PhantomReference<Object>
  2. {
  3.     private static Cleaner first = null;
  4.     private Cleaner next = null, prev = null;
  5. }
复制代码
对于 WeakReference , SoftReference 对象来说,此时如果我们调用它们的 get 方法将会得到一个 null 。
  1.     private static synchronized Cleaner add(Cleaner cl) {
  2.         if (first != null) {
  3.             cl.next = first;
  4.             first.prev = cl;
  5.         }
  6.         first = cl;
  7.         return cl;
  8.     }
复制代码
对于 Cleaner 这个 PhantomReference 来说,此时 Cleaner 与 DirectByteBuffer 的虚引用关系就被 JVM 扫除了。 GC 标记阶段竣事之后,DirectByteBuffer 对象就会被清理掉。

也就是说,当 GC 竣事之后,_reference_pending_list 链表中保存的这些 Cleaner 对象,它们虚引用的 DirectByteBuffer 就已经被回收掉了,此时我们就需要从这个 _reference_pending_list 中把这些 Cleaner 对象一个一个的拿下来,然后调用它的 clean 方法,在 Deallocator 中将这些已经被回收掉的 DirectByteBuffer 背后引用的 native memory 释放掉就可以了。
这个正是 ReferenceHandler 线程所要干的事变,在它的 run 方法中会不绝的调用 processPendingReferences,从 JVM 的  _reference_pending_list 中不绝地将 Cleaner 对象摘下来,调用它的 clean 方法释放 native memory。
当然了 _reference_pending_list 链表中保存的不仅仅是 Cleaner 这个 PhantomReference,还有 WeakReference,SoftReference 对象,但它们共同的特点是这些 Reference 对象所引用的 Java 对象都已经被回收了。
  1.     // 具体资源的释放逻辑封装在 thunk 中
  2.     // Deallocator 对象
  3.     private final Runnable thunk;
  4.     public void clean() {
  5.         // 将该 Cleaner 对象从双向链表中删除,目的是断开 Cleaner 对象的强引用链
  6.         // 等到下一次 GC 的时候就可以回收这个 Cleaner 对象了
  7.         if (!remove(this))
  8.             return;
  9.         try {
  10.             // 进行资源清理
  11.             thunk.run();
  12.         } catch (final Throwable x) {
  13.            .... 省略 ....
  14.         }
  15.     }
复制代码
但是这个 _reference_pending_list 它是 JVM 内部维护的一个链表,它只能在 JVM 内被 GC 线程操纵,我们在外部是无法访问到的。
从 Reference 类中我们就可以看到,Reference 类的内部并没有一个叫做 pending_list 的字段去指向 JVM 内部的 _reference_pending_list,在 JDK 中,所有针对 _reference_pending_list 的访问都是通过 native 方法进行的。
  1.    private static class Deallocator implements Runnable
  2.    {
  3.         // native memory 的起始内存地址
  4.         private long address;
  5.         // OS 实际申请的 native memory 大小
  6.         private long size;
  7.         // DirectByteBuffer 的容量
  8.         private int capacity;
  9.         public void run() {
  10.             if (address == 0) {
  11.                 // Paranoia
  12.                 return;
  13.             }
  14.             // 底层调用 free 来释放 native memory
  15.             UNSAFE.freeMemory(address);
  16.             address = 0;
  17.             // 更新 direct memory 的用量统计信息
  18.             Bits.unreserveMemory(size, capacity);
  19.         }
  20.     }
复制代码
当 _reference_pending_list 中没有 Reference 对象要处理的时间,ReferenceHandler 线程会通过 native 方法 —— waitForReferencePendingList ,在 _reference_pending_list 上壅闭等待。
  1. class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
  2. {
  3.     private final Cleaner cleaner;
  4.     DirectByteBuffer(int cap) {                   // package-private
  5.         ...... 省略 .....   
  6.         cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
  7.     }
  8. }
复制代码
随后 GC 线程在标记阶段竣事之后,会将那些需要被处理的 Reference 对象放到 _reference_pending_list 中,然后唤醒 ReferenceHandler 线程行止理。
当 ReferenceHandler 线程被 JVM 唤醒之后,就会调用 native 方法 —— getAndClearReferencePendingList ,从 JVM 中获取 _reference_pending_list,并保存到一个范例为 Reference 的局部变量 pendingList 中,末了将 JVM 中的 _reference_pending_list 置为 null,方便 JVM 等到下一轮 GC 的时间处理其他 Reference 对象。
  1. public class FileChannelImpl extends FileChannel
  2. {
  3.    public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
  4.   
  5.         Unmapper unmapper = mapInternal(mode, position, size, prot, isSync);
  6.    
  7.         return Util.newMappedByteBuffer((int)unmapper.cap,
  8.                     unmapper.address + unmapper.pagePosition,
  9.                     unmapper.fd,
  10.                     unmapper, isSync);   
  11.     }
  12. }
复制代码
当我们熟悉了  ReferenceHandler 线程怎样与 JVM 中的 _reference_pending_list 交互之后,再回过头来看 processPendingReferences() 方法就很清晰了,这里正是 ReferenceHandler 线程的焦点所在。
  1. class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
  2. {
  3.    protected DirectByteBuffer(int cap, long addr,
  4.                                      FileDescriptor fd,
  5.                                      Runnable unmapper,
  6.                                      boolean isSync, MemorySegmentProxy segment)
  7.     {
  8.         super(-1, 0, cap, cap, fd, isSync, segment);
  9.         address = addr;
  10.         cleaner = Cleaner.create(this, unmapper);
  11.         att = null;
  12.     }
  13. }
复制代码
这里我们先聊一聊 processPendingLock 和 processPendingActive 的作用,它俩的目的是为了维护 ReferenceHandler 线程和 Java 业务线程对于 Reference 处理进度视图的一致性。
什么意思呢 ?我们还是拿 Cleaner 来说,对于 Cleaner 而言,ReferenceHandler 线程扮演的其实是一个资源回收的角色,Java 业务线程扮演的其实是一个资源申请的角色。
当 Java 线程申请资源的时间,如果资源不够了怎么办呢 ?首先应该想到的是先去看看当前系统中有没有正在被回收的资源,对吧。

  • 如果 processPendingActive 为 true,表示 ReferenceHandler 线程正在处理 Cleaner 释放资源
  • 如果 JVM 中的 _reference_pending_list 不为空,说明当前系统中有正在等待回收的资源。
如果以上两个条件成立的话,那么 Java 线程就在 processPendingLock 上等一等,等待 ReferenceHandler 线程把该回收的资源回收完。当本次 GC 所收集到的所有 Cleaner 都已经被 ReferenceHandler 线程处理完之后,ReferenceHandler 线程就会 notify 在 processPendingLock 上等待的 Java 线程,随后 Java 线程再去尝试申请资源。
如果当前系统中并没有正在被回收的资源,比如下面两个条件:

  • processPendingActive 为 false,表示 ReferenceHandler 线程已经将本次 GC 收集到的 Cleaner 全部处理完了。
  • 如果 JVM 中的 _reference_pending_list 为空,说明当前系统中没有可回收的资源。
这种情况下 Java 线程就不需要在 processPendingLock 上等待了,要么就直接触发 OOM,要么就调用 System.gc 尝试让 GC 在去收集一些需要被处理的 Cleaner。
明白了这些,我们接着来看 ReferenceHandler 线程处理 Reference 的主线流程。
首先 ReferenceHandler 线程会将 pendingList 中的 Reference 依次从链表上摘下,随后会判断一下这个 Reference 对象的具体范例,如果是 Cleaner 范例的话,ReferenceHandler 线程就会在这里调用它的 clean 方法。
  1. private static abstract class Unmapper implements Runnable, UnmapperProxy {
  2.     @Override
  3.     public void run() {
  4.             unmap();
  5.     }
  6.     public void unmap() {
  7.             if (address == 0)
  8.                 return;
  9.             // 底层调用 unmmap 系统调用,释放由 mmap 映射出来的虚拟内存以及物理内存
  10.             unmap0(address, size);
  11.             address = 0;
  12.             // if this mapping has a valid file descriptor then we close it
  13.             if (fd.valid()) {
  14.                 try {
  15.                     nd.close(fd);
  16.                 } catch (IOException ignore) {
  17.                     // nothing we can do
  18.                 }
  19.             }
  20.         }
  21.   }
复制代码
在 clean 方法中,会执行 thunk 的 run 方法,在创建 DiretByteBuffer 的时间会将 thunk 指向一个 Deallocator 实例。
  1. public abstract class Reference<T> {
  2.     static {
  3.         ThreadGroup tg = Thread.currentThread().getThreadGroup();
  4.         // 获取 system thread group
  5.         for (ThreadGroup tgn = tg;
  6.              tgn != null;
  7.              tg = tgn, tgn = tg.getParent());
  8.         // 创建 system thread : ReferenceHandler
  9.         Thread handler = new ReferenceHandler(tg, "Reference Handler");
  10.         // 设置 ReferenceHandler 线程的优先级为最高优先级
  11.         handler.setPriority(Thread.MAX_PRIORITY);
  12.         handler.setDaemon(true);
  13.         handler.start();  
  14.     }
  15. }
复制代码
当执行完 clean 方法之后,其实 Cleaner 的历史使命就完成了,但别忘了,Cleaner 内部有一个全局的双向链表,里边强引用着所有的 Cleaner 对象。
  1. // zReferenceProcessor.cpp 文件
  2. OopHandle Universe::_reference_pending_list;
  3. // Create a handle for reference_pending_list
  4. _reference_pending_list = OopHandle(vm_global(), NULL);
复制代码

所以为了让 Cleaner 可以在下一轮 GC 中被回收掉,需要调用 remove 方法将这个 Cleaner 对象从双向链表中摘下,断开这个唯一的强引用。等到下次 GC 的时间,这个 Cleaner 对象就可以被回收了。
好了,现在关于 Cleaner 处理的完整生命周期,笔者就为大家先容完了,但在 pendingList 中除了 Cleaner 之外还有其他范例的 Reference 对象,比如,其他 PhantomReference,WeakReference,SoftReference,FinalReference。
当 ReferenceHandler 线程发现 pendingList 中的 Reference 对象不是 Cleaner,那么就会调用 enqueueFromPending 方法,将这个 Reference 对象添加到与其对应的 ReferenceQueue 中。
  1. public abstract class Reference<T> {
  2.      private transient Reference<?> discovered;
  3. }
复制代码
在 defineClass1 的 native 实现中,会调用到一个 SystemDictionary::resolve_from_stream 方法,在这里会完成类的加载,验证,分析等操纵,在完成类的分析之后就会构建 nonstatic_oop_maps,创建 InstanceKlass 实例,末了将 nonstatic_oop_maps 填充到 InstanceKlass 实例中。
  1. public abstract class Reference<T> {
  2.      private T referent;
  3. }
复制代码
类的加载逻辑主要在 KlassFactory::create_from_stream 中进行:
  1. public T get() {
  2.         return this.referent;
  3. }
复制代码
nonstatic_oop_maps 的构建主要是在类分析阶段之后,由 post_process_parsed_stream  函数负责触发构建。
  1.     private static class ReferenceHandler extends Thread {
  2.         public void run() {
  3.             while (true) {
  4.                 processPendingReferences();
  5.             }
  6.         }
  7.     }
复制代码
在 post_process_parsed_stream 函数中会对 Java 类中定义的所有字段进行结构:
  1. public abstract class Reference<T> {
  2.   /*
  3.      * Atomically get and clear (set to null) the VM's pending-Reference list.
  4.      */
  5.     private static native Reference<?> getAndClearReferencePendingList();
  6.     /*
  7.      * Test whether the VM's pending-Reference list contains any entries.
  8.      */
  9.     private static native boolean hasReferencePendingList();
  10.     /*
  11.      * Wait until the VM's pending-Reference list may be non-null.
  12.      */
  13.     private static native void waitForReferencePendingList();
  14. }
复制代码
在字段结构完成之后,会在 epilogue() 函数中按照字段的结构信息,构建 nonstatic_oop_maps。

  • 首先继承来自其父类的 nonstatic_oop_maps。
  • 从 _root_group->oop_fields() 中获取类中的所有非静态成员变量,并将相邻的成员变量构建在同一个 OopMapBlock 中
  • 处理被 @Contended 标注过的非静态成员变量,属于同一个 content group 的成员变量在对象实例内存中必须连续存放,独占 CPU 缓存行。所以同一个 content group  下的成员变量会被构建在同一个 OopMapBlock 中。
  1. // Reference.c 文件
  2. JNIEXPORT void JNICALL
  3. Java_java_lang_ref_Reference_waitForReferencePendingList(JNIEnv *env, jclass ignore)
  4. {
  5.     JVM_WaitForReferencePendingList(env);
  6. }
  7. // jvm.cpp 文件
  8. JVM_ENTRY(void, JVM_WaitForReferencePendingList(JNIEnv* env))
  9.   MonitorLocker ml(Heap_lock);
  10.   while (!Universe::has_reference_pending_list()) {
  11.     // 如果 _reference_pending_list 还没有 Reference 对象,那么当前线程在 Heap_lock 上 wait
  12.     ml.wait();
  13.   }
  14. JVM_END
  15. // universe.cpp 文件
  16. bool Universe::has_reference_pending_list() {
  17.   assert_pll_ownership();
  18.   // 检查 _reference_pending_list 是否为空
  19.   return _reference_pending_list.peek() != NULL;
  20. }
复制代码
JVM 在完成类的加载,分析,以及构建完 nonstatic_oop_maps 之后,就会为 Java 类在 JVM 中分配一个 InstanceKlass 实例。
  1. // Reference.c 文件
  2. JNIEXPORT jobject JNICALL
  3. Java_java_lang_ref_Reference_getAndClearReferencePendingList(JNIEnv *env, jclass ignore)
  4. {
  5.     return JVM_GetAndClearReferencePendingList(env);
  6. }
  7. // jvm.cpp 文件
  8. JVM_ENTRY(jobject, JVM_GetAndClearReferencePendingList(JNIEnv* env))
  9.   MonitorLocker ml(Heap_lock);
  10.   // 从 JVM 中获取 _reference_pending_list
  11.   oop ref = Universe::reference_pending_list();
  12.   if (ref != NULL) {
  13.     // 将 JVM 中的 _reference_pending_list 置为 null
  14.     // 方便下一轮 GC 处理其他 reference 对象
  15.     Universe::clear_reference_pending_list();
  16.   }
  17.   // 将 _reference_pending_list 返回给 ReferenceHandler 线程
  18.   return JNIHandles::make_local(THREAD, ref);
  19. JVM_END
  20. // universe.cpp 文件
  21. oop Universe::reference_pending_list() {
  22.   if (Thread::current()->is_VM_thread()) {
  23.     assert_pll_locked(is_locked);
  24.   } else {
  25.     assert_pll_ownership();
  26.   }
  27.   // 返回 _reference_pending_list
  28.   return _reference_pending_list.resolve();
  29. }
  30. void Universe::clear_reference_pending_list() {
  31.   assert_pll_ownership();
  32.   // 将 _reference_pending_list 设置为 null
  33.   _reference_pending_list.replace(NULL);
  34. }
复制代码
InstanceKlass::allocate_instance_klass 只是分配了一个空的 InstanceKlass 实例 ,所以需要 fill_instance_klass 函数来填充 InstanceKlass 实例。在这里会将刚刚构建好的 nonstatic_oop_maps 填充到 InstanceKlass 实例中。
  1.     private static final Object processPendingLock = new Object();
  2.     private static boolean processPendingActive = false;
  3.     private static void processPendingReferences() {
  4.         // ReferenceHandler 线程等待 JVM 向 _reference_pending_list 填充 Reference 对象
  5.         // GC 之后,如果有 Reference 对象需要处理,JVM 则将 ReferenceHandler 线程 唤醒
  6.         waitForReferencePendingList();
  7.         // 用于指向 JVM 的 _reference_pending_list
  8.         Reference<?> pendingList;
  9.         synchronized (processPendingLock) {
  10.             // 获取 _reference_pending_list,随后将 _reference_pending_list 置为 null
  11.             // 方便 JVM 在下一轮 GC 处理其他 Reference 对象
  12.             pendingList = getAndClearReferencePendingList();
  13.             // true 表示 ReferenceHandler 线程正在处理 pendingList
  14.             processPendingActive = true;
  15.         }
  16.         // 将 pendingList 中的 Reference 对象挨个从链表中摘下处理
  17.         while (pendingList != null) {
  18.             // 从 pendingList 中摘下 Reference 对象
  19.             Reference<?> ref = pendingList;
  20.             pendingList = ref.discovered;
  21.             ref.discovered = null;
  22.             
  23.             // 如果该 Reference 对象是 Cleaner 类型,那么在这里就会调用它的 clean 方法
  24.             if (ref instanceof Cleaner) {
  25.                  // Cleaner 的 clean 方法就是在这里调用的
  26.                 ((Cleaner)ref).clean();
  27.          
  28.                 synchronized (processPendingLock) {
  29.                     // 将等待在 processPendingLock 上的其他 Java 业务线程唤醒
  30.                     processPendingLock.notifyAll();
  31.                 }
  32.             } else {
  33.                 // 这里处理除 Cleaner 之外的其他 Reference 对象
  34.                 // 比如,其他 PhantomReference,WeakReference,SoftReference,FinalReference
  35.                 // 将他们添加到对应的 ReferenceQueue 中
  36.                 ref.enqueueFromPending();
  37.             }
  38.         }
  39.         // 本次 GC 收集到的 Reference 对象到这里就全部处理完了
  40.         synchronized (processPendingLock) {
  41.             // false 表示 ReferenceHandler 线程已经处理完毕 pendingList
  42.             processPendingActive = false;
  43.             // 唤醒其他等待在 processPendingLock 上 Java 线程。
  44.             processPendingLock.notifyAll();
  45.         }
  46.     }
复制代码
好了,现在我们已经清楚了 JVM 怎样使用这个 nonstatic_oop_maps 来高效的遍历对象的引用关系图,并且也知道了 JVM 在何时 ?又是怎样 ? 将  nonstatic_oop_maps 创建出来并填充到 InstanceKlass 实例中。
有了这些配景知识的铺垫之后,我们再来看 Reference 语义的实现逻辑就很简单了。
4.3 Reference 范例的 OopMapBlock 有何差别

前面我们提到,当 JVM 遍历到一个对象并将其标记为 alive 之后,随后就会从这个平凡 Java 对象的内存模型中将 _klass 指针取出来,对于平凡的 Java 范例来说,它的 _klass 指针指向的是一个 InstanceKlass 实例,而对于  Reference 范例来说,它的 _klass 指针指向的是一个 InstanceRefKlass 实例。
随后会从 InstanceKlass 实例中将 nonstatic_oop_maps 数组取出来,这个 nonstatic_oop_maps 是在类加载的时间被创建并填充到 InstanceKlass 实例中的。
根据 nonstatic_oop_maps 中构建的类中所有非静态成员变量在对象内存中的地址偏移,JVM 可以轻松的获取到对象中成员变量的地址,顺藤摸瓜,再将这些非静态成员变量引用到的对象全部标记为 alive,反复循环这个逻辑,终极会将整个引用关系图遍历标记完毕。

但是别忘了 Reference 范例本质上也是一个 Java 类,referent 也是 Reference 类中定义的一个非静态成员变量。
  1.     public void clean() {
  2.         if (!remove(this))
  3.             return;
  4.         try {
  5.             // 进行资源清理
  6.             thunk.run();
  7.         } catch (final Throwable x) {
  8.            .... 省略 ....
  9.         }
  10.     }
复制代码
如果按照这个逻辑,JVM 是不是也可以通过 nonstatic_oop_maps 获取到 referent 的内存地址 ,近而将 Reference 引用的对象标记为 alive 呢 ?但是现实是,这个 referent 并没有被 JVM 标记到。

这就有点奇怪了是吧,JVM 是怎么做到的呢 ?Reference 的语义是怎样实现的呢 ?
我们重新来捋一捋,现在的现象是什么 ? 是 Reference 对象的非静态成员变量 referent 没有被标记到对吧。那么查找一个对象的非静态成员变量靠什么 ? 靠的是不是就是我们前面花了大量篇幅先容的 OopMapBlock ?那这个 OopMapBlock 从哪里来的 ?对于平凡对象是不是在它的 InstanceKlass 实例中,对于 Reference 范例的对象是不是在它的 InstanceRefKlass 实例 中 ?
那为什么对于平凡对象来说,可以通过 OopMapBlock 遍历到它的非静态成员变量,而对于 Reference 对象来说,就无法通过 OopMapBlock 遍历到它的 referent 呢 ?
岂非是 JVM 对于 InstanceRefKlass 的 nonstatic_oop_maps 进行了一系列的魔改,压根就没有为 referent 在 OopMapBlock 中建立索引 ?如许自然就不会遍历到 referent,也无法将它标记为 alive 了。
究竟上,JVM 就是这么干的,那么在哪里,又是怎样对 InstanceRefKlass 进行魔改的呢 ?
在 JVM 启动的时间会对 SystemDictionary 进行初始化,SystemDictionary 在 JVM 中的角色是用于管理系统中已经加载的所有 class 类,在 SystemDictionary 初始化的时间会调用到一个重要的函数 InstanceRefKlass::update_nonstatic_oop_maps 。
  1.    private static class Deallocator implements Runnable
  2.    {
  3.         public void run() {   
  4.             // 底层调用 free 来释放 native memory
  5.             UNSAFE.freeMemory(address);
  6.         }
  7.     }
复制代码
从函数命名上,我们就可以看出来,这里就是对 InstanceRefKlass 进行魔改的地方了。
  1. public class Cleaner extends PhantomReference<Object>
  2. {
  3.     private static Cleaner first = null;
  4.     private Cleaner next = null, prev = null;
  5. }
复制代码
在 JVM 启动的时间会对所有底子类进行加载当然也包含 Reference 类,宁静凡的 Java 范例一样,Reference 类被加载之后,JVM 也会为它构建一个全量的 nonstatic_oop_maps,里面确实也包含了所有的非静态成员变量(referent 字段也包括在内)。
随后就会在 update_nonstatic_oop_maps  中对 InstanceRefKlass 进行魔改。
  1.     public void clean() {
  2.         if (!remove(this))
  3.             return;
  4.         try {
  5.             // 进行资源清理
  6.             thunk.run();
  7.         } catch (final Throwable x) {
  8.            .... 省略 ....
  9.         }
  10.     }
复制代码
首先会通过 java_lang_ref_Reference::queue_offset() 将成员变量 queue 的地址偏移取出来 —— new_offset,然后将原来 OopMapBlock 的 _count 设置为 2 ,用新的 new_offset,new_count 重新构建 OopMapBlock。
这里 new_count 设置为 2 的意思就是,只将 Reference 类中的非静态成员变量 queue 和 next 构建到 OopMapBlock 中。
也就是说,当 JVM 遍历到一个 Reference 对象时,只能通过它的 OopMapBlock 遍历到 queue 和 next,无法遍历到 referent 和 discovered。
经过如许的魔改之后,JVM 就巧妙地实现了 Reference 的语义。大家这里可以停下来追念追念 WeakReference 的语义,是不是就实现了当一个 Java 对象只存在一条弱引用链的时间,发生 GC 的时间,只被弱引用所关联的对象就会被回收掉。本质原因就是这个被 JVM 魔改之后的 OopMapBlock 产生了作用。
有同学可能会问了,你说的只是 WeakReference 的语义啊,Reference 又不只是 WeakReference 这一种,还有 SoftReference,PhantomReference,FinalReference 这些 Reference 范例,似乎在这一末节中并没有看到他们的语义实现。
究竟上,笔者在这一末节中只是为大家揭破 Reference 最为本质的面貌,SoftReference,PhantomReference,FinalReference  这些具体的语义都是在 WeakReference 语义的底子上进行了小小的魔改而已,等笔者把该铺垫的配景知识全部铺垫好,背面会有单独的末节专门为大家解释清楚其他 Reference 范例的语义实现。
5. JVM 在 GC 的时间怎样处理 Reference

在本文的第三末节中,我们主要在 JVM 的外围来讨论 JDK 怎样通过 ReferenceHandler 线程来处理 Reference 对象,其中提到 JVM 内部有一个非常重要的 _reference_pending_list 链表,当 Reference 的 referent 对象没有任何强引用链或者软引用链可达时,GC 线程就会回收这个 referent 对象。那么与之对应的 Reference 对象就会被 JVM 接纳头插法的方式插入到这个 _reference_pending_list 中。
  1. public abstract class Reference<T> {
  2.     volatile Reference next;
  3. }
复制代码
如果 _reference_pending_list 中没有任何需要被处理的 Reference 对象时,ReferenceHandler 线程就会在一个 native 方法 —— waitForReferencePendingList() 上壅闭等待。
当发生 GC 的时间,JVM 就会从 GcRoot 开始挨个遍历整个引用关系图中的对象,并将遍历到的对象标记为 alive,没有被标记到的对象就会被 JVM 当做垃圾回收掉。
当 referent 对象没有被标记到,需要被 GC 线程回收的时间,JVM 就会将与它关联的 Reference 插入到 _reference_pending_list 中,并唤醒 ReferenceHandler 线程行止理,背面的内容我们在第三末节中已经详细的讨论过了。

本末节中,笔者将带着大家深入到 JVM 内部,看看发生 GC 的时间,JVM 怎样处理这些 Reference 对象 ? 怎样判断哪些 Reference 需要被插入到 _reference_pending_list 中? 和我们前面第三末节中的内容遥相呼应起来,如许一来我们就从 JDK 层面再到 JVM 层面将整个 Reference 的处理链路打通了。
下面笔者就以 ZGC 为例,带着大家看一看 JVM 内部到底是怎样处理 Reference 的 :
  1.     Reference(T referent, ReferenceQueue<? super T> queue) {
  2.         // 被引用的普通 Java 对象
  3.         this.referent = referent;
  4.         // 全局 ReferenceQueue 实例
  5.         this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
  6.     }
复制代码
ZGC 整个 GC 过程分为 10 个阶段,其中只有四个阶段需要非常短暂的 STW,剩下的六个阶段全部是与 Java 应用线程并发执行的,阶段固然比较多,整个 GC 过程也非常的复杂,但与本末节相关的阶段只有两个,分别是第二阶段的并发标记阶段 —— Phase 2: Concurrent Mark,与第五阶段的并发处理非强引用 Reference 阶段 —— Phase 5: Concurrent Process Non-Strong References。
其中 Concurrent Mark 主要的任务就是从 GcRoot 开始并发标记根对象,并沿着根对象遍历整个堆中的引用关系,在整个遍历的过程中会逐渐发现那些需要被 ReferenceHandler 线程处理的 Reference 对象,随后会将这些 Reference 对象插入到 _discovered_list 中。
这里大家可能会有疑问,你刚才不是说 JVM 会将需要被处理的 Reference 对象插入到 _reference_pending_list 中吗 ?怎么现在又酿成 _discovered_list 了 ?
究竟上,大家可以将 _discovered_list 理解为一个临时的 _reference_pending_list,在 ZGC 的整个过程中会用到两个临时的 _reference_pending_list,它们分别是 _discovered_list,_pending_list。
  1. public Reference<? extends T> poll()
复制代码
ZGC 有多个 GC 线程负责并发执行垃圾回收任务,_discovered_list 是 ZPerWorker 范例的,每一个 GC 线程都有一个 _discovered_list,负责临时存储由该 GC 线程在并发标记过程中发现的 Reference 对象。
在并发标记竣事之后,这些 GC 线程就会将各自在 _discovered_list 中收集到的 Reference 对象统一转移到 _pending_list 中,_pending_list 在所有 GC 线程中是共享的,负责汇总 ZGC 线程收集到的所有 Reference 对象。
在 Concurrent Process Non-Strong References 阶段的末了,JVM 会将 _pending_list 中汇总的 Reference 对象再次统一转移到 _reference_pending_list 中,_reference_pending_list 是终极对外的发布形态,ReferenceHandler 线程只会和 _reference_pending_list 打交道。
理解了这个配景,下面我们就来一起看下 Concurrent Mark 阶段是怎样发现 Reference 对象的
5.1 Concurrent Mark

当 ZGC 遍历到一个对象 —— oop obj 并将其标记为 alive 之后,就会调用 follow_object  方法,来遍历 obj 的所有非静态成员变量,然后将这些成员变量所引用的 obj 标记为 alive,然后再次调用 follow_object 继续遍历引用关系图,如许循环往复。 ZGC  就是靠着这个 follow_object 方法驱动着所有 GC 线程去遍历整个堆的引用关系图。
  1. public Reference<? extends T> remove(long timeout)
  2.         throws IllegalArgumentException, InterruptedException
复制代码
这里就来到了笔者在第四末节中先容的内容,这个函数熟悉吗 ?没印象的话再去回顾下第四末节。
  1. public static void main(String[] args) throws Exception{
  2.         ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
  3.         Object phantomObject = new Object();
  4.         Reference phantomReference = new  PhantomReference(phantomObject,referenceQueue);
  5.         Object weakObject = new Object();
  6.         Reference weakReference = new WeakReference(weakObject, referenceQueue);
  7.         Object softObject = new Object();
  8.         Reference softReference = new SoftReference(softObject, referenceQueue);
  9.         phantomObject = null;
  10.         weakObject = null;
  11.         // 当内存不足的时候,softObject 才会被回收
  12.         softObject = null;
  13.         
  14.         System.gc();
  15.         Reference getReferenceByPoll = referenceQueue.poll();
  16.         long timeout = 1000;
  17.         Reference getReferenceByRemove = referenceQueue.remove(timeout);
  18.     }
复制代码
首先会通过 klass() 函数去获取 obj 中的 _klass 指针,对于平凡范例的 Java 对象来说,_klass 指向的是 InstanceKlass 实例,对于 Reference 范例的对象来说,_klass 指向的是 InstanceRefKlass 实例。
终极的遍历动作是在对应 Klass 中的 oop_oop_iterate 方法中进行的,本末节我们重点关注 InstanceRefKlass。
  1. public class WeakHashMap<K,V> extends AbstractMap<K,V>  implements Map<K,V> {
  2.     Entry<K,V>[] table;
  3.     private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
  4. }
复制代码
首先会调用 InstanceKlass:op_oop_iterate 函数,这个函数熟悉吗 ?我们在第四末节中重点先容的就是这个函数。
在这个函数中获取 InstanceRefKlass 实例中的 nonstatic_oop_maps,通过 OopMapBlock 去遍历标记 Reference 对象非静态成员变量。
  1.     private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
  2.         V value;
  3.         final int hash;
  4.         Entry<K,V> next;
  5.         /**
  6.          * Creates new entry.
  7.          */
  8.         Entry(Object key, V value,
  9.               ReferenceQueue<Object> queue,
  10.               int hash, Entry<K,V> next) {
  11.             super(key, queue);
  12.             this.value = value;
  13.             this.hash  = hash;
  14.             this.next  = next;
  15.         }
  16. }
复制代码
但笔者前面先容过,InstanceRefKlass 中的 nonstatic_oop_maps 是被 JVM  经过特殊魔改的,这里并不会遍历到 Reference 对象的 referent 字段和 discovered 字段。
  1.     public void clean() {
  2.         if (!remove(this))
  3.             return;
  4.         try {
  5.             // 进行资源清理
  6.             thunk.run();
  7.         } catch (final Throwable x) {
  8.            .... 省略 ....
  9.         }
  10.     }
复制代码
在遍历标记完  Reference 对象的非静态成员变量之后,JVM 会调用
oop_oop_iterate_ref_processing 来判断该 Reference 对象是否应该插入到 _discovered_list 中。
  1.     private void expungeStaleEntries() {
  2.         for (Object x; (x = queue.poll()) != null; ) {
  3.             synchronized (queue) {
  4.              ... 将 ReferenceQueue 中的 Entry 全部从 WeakHashMap 中删除 ...
  5.             }
  6.         }
  7.     }
复制代码
  1. class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
  2. {
  3.     private final Cleaner cleaner;
  4.     DirectByteBuffer(int cap) {                   // package-private
  5.         ...... 省略 .....   
  6.         // 检查堆外内存整体用量是否超过了 -XX:MaxDirectMemorySize
  7.         // 如果超过则尝试等待一下 JVM 回收堆外内存,回收之后还不够的话则抛出 OutOfMemoryError
  8.         Bits.reserveMemory(size, cap);   
  9.         // 底层调用 malloc 申请虚拟内存
  10.         base = UNSAFE.allocateMemory(size);
  11.         ...... 省略 .....   
  12.         cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
  13.     }
  14. }
复制代码
在 try_discover 中,JVM 首先会通过 load_referent 从堆中加载 Reference 引用的 referent 对象。这里会判断 referent 对象是否已经被 GC 线程标记过了,如果已经被标记了,说明 referent 是 alive 的,那么这个 Reference 对象就不需要被放入 _discovered_list 中,直接 return 掉。
如果 referent 没有被标记,则进入 ZReferenceProcessor->discover_reference  函数中作进一步的 discover 逻辑判断。
  1.     // -XX:MaxDirectMemorySize 最大允许使用的 direct memory 容量
  2.     private static volatile long MAX_MEMORY = VM.maxDirectMemory();
  3.     // 向 OS 实际申请的内存,考虑到内存对齐的情况,实际向 OS 申请的内存会比指定的 cap 要多
  4.     private static final AtomicLong RESERVED_MEMORY = new AtomicLong();
  5.     // 已经使用的 direct memory 总量
  6.     private static final AtomicLong TOTAL_CAPACITY = new AtomicLong();
  7.     private static boolean tryReserveMemory(long size, long cap) {
  8.         long totalCap;
  9.         while (cap <= MAX_MEMORY - (totalCap = TOTAL_CAPACITY.get())) {
  10.             if (TOTAL_CAPACITY.compareAndSet(totalCap, totalCap + cap)) {
  11.                 RESERVED_MEMORY.addAndGet(size);
  12.                 COUNT.incrementAndGet();
  13.                 return true;
  14.             }
  15.         }
  16.         // 已经超过了最大 direct memory 容量的限制则返回 false
  17.         return false;
  18.     }
复制代码
discover_reference 的逻辑很简单,主要分为两步:

  • 通过 should_discover 判断该 Reference 对象是否需要被 ReferenceHandler 线程处理
  • 如果 Reference 对象需要被处理的话就通过 discover  方法,将其插入到 _discovered_list 中。
  1. public abstract class Reference<T> {
  2.     private static boolean waitForReferenceProcessing()
  3.         throws InterruptedException
  4.     {
  5.         synchronized (processPendingLock) {
  6.             // processPendingActive = true 表示 ReferenceHandler 线程正在处理 PendingList 中的 Cleaner,那么就等待 ReferenceHandler 处理完
  7.             // hasReferencePendingList 检查 JVM 中的 _reference_pending_list 是否包含待处理的 Reference 对象
  8.             // 如果还有待处理的 Reference,那么也等待一下
  9.             if (processPendingActive || hasReferencePendingList()) {
  10.                 // 等待 ReferenceHandler 线程处理 Cleaner 释放 direct memory
  11.                 processPendingLock.wait();
  12.                 return true;
  13.             } else {
  14.                 // 当前系统中没有待处理的 Reference,直接返回 false
  15.                 return false;
  16.             }
  17.         }
  18.     }
  19. }
复制代码
should_discover 判断是否将 Reference 添加到 _discovered_list 中的逻辑依据主要有三个方面:
如果 Reference 对象的状态是 inactive,那么 JVM 就不会将它放入 _discovered_list 中。那么什么时间 Reference 对象会变为  inactive 呢 ?
比如,应用线程自己调用 Reference.enqueue() 方法,自己亲身将 Reference 对象添加到与其关联的 ReferenceQueue 中等待进一步的处理。那么这里 JVM 就不需要将 Reference 添加到 _discovered_list 中了。
因为终极 ReferenceHandler 线程还是会从 _reference_pending_list 中将  Reference 添加到 ReferenceQueue 中,如许一来就重复了。应用线程在调用  Reference.enqueue() 方法之后,Reference 的状态就变为了 inactive 。
还有一种变为 inactive 的情况就是应用线程直接调用 Reference.clear() 方法,表示应用线程自己已经处理过 Reference 对象了,JVM 就别管了,此时 Reference 的状态变为 inactive , 那么在下一轮 GC 的时间该 Reference 对象就会被回收,并且不会再次被添加到 _discovered_list 中。
这也就解释了为什么 Reference 状态变为 inactive 之后,JVM 将不会再次将其放入 _discovered_list 的原因了,因为它已经被处理过了。处于 inactive 状态的 Reference 有一个共同的特点就是它的 referent = null。
第二个条件是如果它的 referent 仍然存在强引用链,那么这个 Reference 将不会被放入 _discovered_list。
第三个条件是如果它的 referent 仍然存在软引用链,也就是还被软引用所关联,如果此时内存充足,软引用不会被回收的话,那么这个 Reference 也不会被放入 _discovered_list。
  1.     static void reserveMemory(long size, long cap) {
  2.          // 首先检查一下 direct memory 的使用量是否已经超过了 -XX:MaxDirectMemorySize 的限制
  3.         if (tryReserveMemory(size, cap)) {
  4.             return;
  5.         }
  6.         final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
  7.         boolean interrupted = false;
  8.         try {      
  9.             boolean refprocActive;
  10.             do {
  11.                 try {
  12.                     // refprocActive = true 表示 ReferenceHandler 线程又释放了一些 direct memory
  13.                     // refprocActive = false 表示当前系统中没有待处理的 Cleaner,系统中已经没有任何可回收的 direct memory 了
  14.                     refprocActive = jlra.waitForReferenceProcessing();
  15.                 } catch (InterruptedException e) {
  16.                     // Defer interrupts and keep trying.
  17.                     interrupted = true;
  18.                     refprocActive = true;
  19.                 }
  20.                 // 再次检查 direct memory 的容量是否能够满足本次分配请求
  21.                 if (tryReserveMemory(size, cap)) {
  22.                     return;
  23.                 }
  24.             } while (refprocActive);
  25.             // 此时系统中已经没有任何可回收的 direct memory 了
  26.             // 只能触发 gc,尝试让 JVM 再去回收一些没有任何强引用的 directByteBuffer
  27.             System.gc();
  28.             
  29.             // 下面开始睡眠等待 ReferenceHandler 线程调用 Cleaner 释放 direct memory
  30.             // 初始睡眠时间, 单位 ms
  31.             long sleepTime = 1;
  32.             // 睡眠次数,最多睡眠 9 次
  33.             int sleeps = 0;
  34.             while (true) {
  35.                 if (tryReserveMemory(size, cap)) {
  36.                     return;
  37.                 }
  38.                 // MAX_SLEEPS = 9
  39.                 if (sleeps >= MAX_SLEEPS) {
  40.                     break;
  41.                 }
  42.                 try {
  43.                     // 等待 ReferenceHandler 线程处理 Cleaner 释放 direct memory (返回 true)
  44.                     // 当前系统中没有任何可回收的 direct memory,则 Thread.sleep 睡眠 (返回 false)
  45.                     if (!jlra.waitForReferenceProcessing()) {
  46.                         // 睡眠等待其他线程触发 gc,尝试看看后面几轮 gc 是否能够回收到一点 direct memory
  47.                         // 最多睡眠 9 次,每次睡眠时间按照 1, 2, 4, 8, 16, 32, 64, 128, 256 ms 依次递增
  48.                         Thread.sleep(sleepTime);
  49.                         sleepTime <<= 1;
  50.                         sleeps++;
  51.                     }
  52.                 } catch (InterruptedException e) {
  53.                     interrupted = true;
  54.                 }
  55.             }
  56.             // 在尝试回收 direct memory 511 ms 后触发 OOM
  57.             throw new OutOfMemoryError
  58.                 ("Cannot reserve "
  59.                  + size + " bytes of direct buffer memory (allocated: "
  60.                  + RESERVED_MEMORY.get() + ", limit: " + MAX_MEMORY +")");
  61.         } finally {
  62.         }
  63.     }
复制代码
如果 Reference 对象的 referent 在当前堆中已经没有任何强引用或者软引用了,并且该 Reference 对象不是 inactive 状态的,那么 JVM 就会将该 Reference 对象通过下面的 discover 方法插入到 _discovered_list 中(头插法)。
  1. public abstract class Reference<T> {
  2.   private T referent;
  3. }
复制代码
从以上过程我们可以看出,在 ZGC 的 Concurrent Mark 阶段, Reference 对象被 JVM 添加到 _discovered_list 中需要同时符合下面四个条件:

  • Reference 对象引用的 referent 没有被 GC 标记过。
  • Reference 对象的状态不能是 inactive, 也就是说这个 Reference 还没有被应用线程处理过,Reference 之前没有参加过 _discovered_list。
  • referent 不存在任何强引用链。
  • referent 不存在任何软引用链。

好了,现在 Reference 在 Concurrent Mark 阶段的处理过程,笔者就为大家先容完了,这里需要留意的是,目前 _discovered_list 中收集到的 Reference 都只是临时的,因为当前所处的阶段为并发标记阶段,应用线程和 GC 线程是并发执行的,再加上标记阶段还没有竣事,所以 Reference 参加到 _discovered_list 的条件可能随时会被应用线程和 GC 线程再次改变。
_discovered_list 终态的确定需要等到并发标记阶段完全竣事,在 ZGC 的第五阶段 —— Concurrent Process Non-Strong References 进行终极的处理。
5.2 Concurrent Process Non-Strong References
  1. // Describes where oops are located in instances of this klass.
  2. class OopMapBlock {
  3. public:
  4.   // Byte offset of the first oop mapped by this block.
  5.   int offset() const          { return _offset; }
  6.   void set_offset(int offset) { _offset = offset; }
  7.   // Number of oops in this block.
  8.   uint count() const         { return _count; }
  9.   void set_count(uint count) { _count = count; }
  10. private:
  11.   int  _offset;
  12.   uint _count;
  13. };
复制代码
ZGC 在 Concurrent Process Non-Strong References 阶段对于 Reference 的终极处理是在 ZReferenceProcessor 中完成的,其中主要包括两个焦点步骤:
首先在 process_references() 函数中,判断 ZGC 在 Concurrent Mark 阶段的  _discovered_list 中收集到的临时 Reference 对象所引用的 referent 是否存活,如果这些 referent 仍然存活,那么就需要将对应的 Reference 对象从 _discovered_list 中移除。
如果这些 referent 不再存活,那么就将与其关联的 Reference 对象继续保留在 _discovered_list,末了将 _discovered_list 中依然保留的 Reference 对象添加到 _pending_list 中,然后清空 _discovered_list。
第二个步骤就是在 enqueue_references() 函数中,将终极确定下来的 _pending_list 再次添加到 _reference_pending_list 中,随后唤醒 ReferenceHandler 线程行止理 _reference_pending_list 中的 Reference 对象,末了清空 _pending_list,为下一轮 GC  做准备。
以上就是 ZGC 对于 Non-Strong References 的总体处理流程,下面我们就来看下这两个焦点步骤中的具体处理细节:
  1. InstanceKlass* InstanceKlass::allocate_instance_klass(const ClassFileParser& parser, TRAPS) {
  2.   // InstanceKlass 实例的内存布局
  3.   const int size = InstanceKlass::size(parser.vtable_size(),
  4.                                        parser.itable_size(),
  5.                                        nonstatic_oop_map_size(parser.total_oop_map_count()),
  6.                                        parser.is_interface());
  7.   const Symbol* const class_name = parser.class_name();
  8.   assert(class_name != NULL, "invariant");
  9.   ClassLoaderData* loader_data = parser.loader_data();
  10.   assert(loader_data != NULL, "invariant");
  11.   InstanceKlass* ik;
  12.   // Allocation
  13.   if (REF_NONE == parser.reference_type()) {
  14.     if (class_name == vmSymbols::java_lang_Class()) {
  15.           ...... 省略 ....
  16.     }
  17.     else if (is_class_loader(class_name, parser)) {
  18.           ...... 省略 ....
  19.     } else {
  20.       // 对于普通的 Java 类来说,这里创建的是 InstanceKlass 实例
  21.       ik = new (loader_data, size, THREAD) InstanceKlass(parser, InstanceKlass::_kind_other);
  22.     }
  23.   } else {
  24.     // 对于 Reference 类来说,这里创建的是 InstanceRefKlass 实例
  25.     ik = new (loader_data, size, THREAD) InstanceRefKlass(parser);
  26.   }
  27.   return ik;
  28. }
复制代码
process_references() 对于 _discovered_list 的处理逻辑被封装在一个 ZReferenceProcessorTask 中,由所有 GC 线程来一起并发执行这个 Task。
  1. inline OopMapBlock* InstanceKlass::start_of_nonstatic_oop_maps() const {
  2.   return (OopMapBlock*)(start_of_itable() + itable_length());
  3. }
  4. inline intptr_t* InstanceKlass::start_of_itable()   const { return (intptr_t*)start_of_vtable() + vtable_length(); }
复制代码
首先通过 _discovered_list.addr() 获取 GC 线程的本地 _discovered_list,前面我们提到 _discovered_list 是一个 ZPerWorker 范例的,每一个 GC 线程对应一个,用于在 Concurrent Mark 阶段并发  discover Reference。
循环遍历 _discovered_list,挨个获取链表中收集到的临时 Reference,通过 should_drop 方法判断是否需要将 Reference 对象从 _discovered_list 中移除。移除条件有两个:

  • 如果 Reference 对象的 referent 被置为 null , 那么就需要将这里的  Reference 对象移撤除。因为在 Reference 被放入到  _pending_list 之前,JVM 会自动调用 Reference 对象的 clear 方法,将 referent 置空。referent = null  代表的语义是这个 Reference 之前已经被添加到 _discovered_list 中了,比如在上一轮 GC 中就已经被处理了,本轮 GC 直接将 Reference 对象回收掉就好了,不需要再重复添加到 _discovered_list。
  • 如果 Reference 对象在前几轮 GC 没有被处理过,是在本轮 GC 中新发现的,那么就继续判断它的 referent 是否还存活,如果仍然存活的话,就将 Reference 对象移除,因为 referent 还在世,自然也不需要被 ReferenceHandler 线程处理
  1. class oopDesc {
  2. private:
  3.   volatile markWord _mark;
  4.   union _metadata {
  5.     Klass*      _klass;
  6.     narrowKlass _compressed_klass;
  7.   } _metadata;
  8. }
复制代码
这里大家可能就有点懵了,因为笔者前面先容过,在 ZGC 的 Concurrent Mark 阶段, Reference 对象被 JVM 添加到 _discovered_list 中的条件就是这个 Reference 对象的 referent 没有被标记过。那为什么这里又要判断一下呢 ?我们来看一个如许的场景:

上图中展示的这个场景是,一个 object 对象在 JVM 堆中同时被一个 StrongReference 对象和一个 WeakReference 对象所引用。
假设在  ZGC 的 Concurrent Mark 阶段,GC 线程先遍历到 WeakReference 对象,留意此时还没有遍历到 StrongReference 对象。由于还没有遍历到 StrongReference ,所以这个 object 对象还没有被标记为 alive。
而对于 WeakReference 对象来说,GC 线程并不会遍历标记它的 referent,对吧,这是我们第四末节中的内容了。这时这个 WeakReference 对象就会被 JVM 添加到 _discovered_list 中。
好的,我们继续 Concurrent Mark,背面 GC 线程终极是要遍历到  StrongReference 对象的,对吧。当 GC 线程遍历到 StrongReference 对象的时间首先会标记这个 StrongReference 对象为 alive,随后开始遍历它的所有非静态成员变量,逐个进行标记 alive,在这个过程中 object 对象终极也会被标记为 alive。
当 Concurrent Mark 竣事之后,我们来到了本末节的 Concurrent Process Non-Strong References 阶段,那么对于此时被添加到  _discovered_list  中的这个 WeakReference 对象是不是就不对了,因为它的 referent 背面又被标记为 alive 了,所以在 should_drop  函数的末了还是要通过 is_alive_barrier_on_weak_oop  判断一下 referent 是否被标记,如果被标记过了,那么就需要将这个 WeakReference 对象从 _discovered_list 中移除。
了解了这个配景,我们再来看 ZReferenceProcessor::work 中的处理逻辑就很清晰了。首先 GC 线程会在 while (*p != NULL) 循环中不绝的遍历 _discovered_list 中临时存放的这些 Reference 对象。
然后通过 should_drop 判断这个 Reference 对象是否应该从 _discovered_list 中移除,如果 should_drop 返回 true ,那么 JVM 就会通过 drop  方法将 Reference 对象移除。很简单的链表操纵,这里笔者就不展开了。
如果 should_drop 返回 false, JVM  就会让这个 Reference 对象继续保留在 _discovered_list 中,并调用 keep 方法获取该 Reference 对象在 _discovered_list 中的下一个元素,继续进行 while 循环重复上述的判断逻辑。
  1. Klass* oopDesc::klass() const {
  2.   if (UseCompressedClassPointers) {
  3.     // 开启压缩指针的情况
  4.     return CompressedKlassPointers::decode_not_null(_metadata._compressed_klass);
  5.   } else {
  6.     return _metadata._klass;
  7.   }
  8. }
复制代码
在 keep 方法中会调用一个 make_inactive 方法,JVM 在这里会调用 Reference 对象的 clear 方法将 referent 置为 null 。
  1. template <typename OopClosureType>
  2. void oopDesc::oop_iterate(OopClosureType* cl) {
  3.   // 遍历对象的所有非静态成员变量
  4.   OopIteratorClosureDispatch::oop_oop_iterate(cl, this, klass());
  5. }
复制代码
那么此时如果我们在应用线程中调用这个 Reference 对象的 get() 方法的时间就会得到一个 null 值,referent 对象被 JVM 置为 null 的时机就是这个 Reference 对象确定要被添加到 _pending_list 的时间。
  1. template <typename T, class OopClosureType>
  2. ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_maps(oop obj, OopClosureType* closure) {
  3.   // InstanceKlass 中有多个 OopMapBlock,它们在 InstanceKlass 实例内存中会放在一起
  4.   // 获取首个 OopMapBlock 地址
  5.   OopMapBlock* map           = start_of_nonstatic_oop_maps();
  6.   // 获取 InstanceKlass 中包含的 OopMapBlock 个数,这些都是在类加载的时候决定的
  7.   // class 文件中有字段表,在类加载的时候可以根据字段表建立 OopMapBlock
  8.   OopMapBlock* const end_map = map + nonstatic_oop_map_count();
  9.   // 挨个遍历 InstanceKlass 中所有的 OopMapBlock
  10.   for (; map < end_map; ++map) {
  11.     // OopMapBlock 中包含的是 java 类中非静态成员变量在对象地址中的偏移
  12.     // 通过它直接可以获取到成员变量的指针
  13.     oop_oop_iterate_oop_map<T>(map, obj, closure);
  14.   }
  15. }
复制代码
当 _discovered_list 中的那些所有需要被移除的 Reference 对象都已经被移除之后,JVM 就会将终态的 _discovered_list 原子地添加到 _pending_list 中。
在  Concurrent Process Non-Strong References 阶段的末了,ZGC 就会调用 enqueue_references 方法将 _pending_list 中的 Reference 对象转移到 _reference_pending_list  中。末了重置 pending list,为下一轮 GC 做准备。
  1. template <typename T, class OopClosureType>
  2. ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_map(OopMapBlock* map, oop obj, OopClosureType* closure) {
  3.   // 通过成员变量在 obj 对象内存中的偏移获取成员变量指针
  4.   T* p         = (T*)obj->obj_field_addr<T>(map->offset());
  5.   // 获取该 OopMapBlock 所映射的成员变量个数
  6.   T* const end = p + map->count();
  7.   // 遍历成员变量挨个标记
  8.   for (; p < end; ++p) {
  9.     // 标记成员变量
  10.     Devirtualizer::do_oop(closure, p);
  11.   }
  12. }
复制代码
这里我们看到 ZGC 在更新完 _reference_pending_list 之后,会调用一个 ml.notify_all(),那么这个操纵是要唤醒谁呢 ?或者说谁会在 Heap_lock 上等待呢 ?
还记不记得笔者在第三末节中为大家先容的 native 方法 —— waitForReferencePendingList() :
  1. public abstract class ClassLoader {
  2.     static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
  3.                                         ProtectionDomain pd, String source);
  4. }
复制代码
那么谁会在这个方法上壅闭等待呢 ?答案就是 —— ReferenceHandler 线程。
  1. static jclass jvm_define_class_common(const char *name,
  2.                                       jobject loader, const jbyte *buf,
  3.                                       jsize len, jobject pd, const char *source,
  4.                                       TRAPS) {
  5.   // 将 class 文件的二进制字节流转换为 ClassFileStream
  6.   ClassFileStream st((u1*)buf, len, source, ClassFileStream::verify);
  7.   // 进行类的加载,验证,解析,随后为创建 InstanceKlass 实例
  8.   Klass* k = SystemDictionary::resolve_from_stream(&st, class_name,
  9.                                                    class_loader,
  10.                                                    cl_info,
  11.                                                    CHECK_NULL);
  12. }
  13. InstanceKlass* SystemDictionary::resolve_class_from_stream
  14.   if (k == NULL) {
  15.     k = KlassFactory::create_from_stream(st, class_name, loader_data, cl_info, CHECK_NULL);
  16.   }
  17. }
复制代码
至于  ReferenceHandler 线程被唤醒之后干了什么 ? 这不就是笔者在第三末节中详细为大家先容的内容么,如许一来是不是就和前面的内容遥相呼应起来了~~~

6. SoftReference 具体在什么时间被回收 ? 怎样量化内存不敷 ?

大家在网上或者在其他讲解 JVM 的书籍中多多少少会看到如许一段关于 SoftReference 的描述 —— “当 SoftReference 所引用的 referent 对象在整个堆中没有其他强引用的时间,发生 GC 的时间,如果此时内存充足,那么这个 referent 对象就和其他强引用一样,不会被 GC 掉,如果此时内存不敷,系统即将 OOM 之前,那么这个 referent 对象就会被当做垃圾回收掉”。

当然了,如果仅从概念上理解的话,如许描述就够了,但是如果我们从 JVM 的实现角度上来说,那如许的描述至少是不准确的,为什么呢 ? 笔者先提两个问题出来,大家可以先思考下:

  • 内存充足的情况下,SoftReference 所引用的 referent 对象就一定不会被回收吗 ?
  • 什么是内存不敷 ?这个概念怎样量化,SoftReference 所引用的 referent 对象到底什么时间被回收 ?
下面笔者继续以 ZGC 为例,带大家深入到 JVM 内部去探寻下这两个问题的精确答案~~
6.1 JVM 无条件回收 SoftReference 的场景

经过前面第五末节的先容,我们知道 ZGC 在 Concurrent Mark 以及 Concurrent Process Non-Strong References 阶段中处理 Reference 对象的关键逻辑都封装在 ZReferenceProcessor 中。
在 ZReferenceProcessor 中有一个关键的属性 —— _soft_reference_policy,在 ZGC  的过程中,处理 SoftReference 的策略就封装在这里,本末节开头提出的那两个问题的答案就隐藏在 _soft_reference_policy 中。
  1. InstanceKlass* KlassFactory::create_from_stream(ClassFileStream* stream,
  2.                                                 Symbol* name,
  3.                                                 ClassLoaderData* loader_data,
  4.                                                 const ClassLoadInfo& cl_info,
  5.                                                 TRAPS) {
  6.   // 这里完成类的加载,验证,解析 以及 nonstatic_oop_maps 的构建
  7.   ClassFileParser parser(stream,
  8.                          name,
  9.                          loader_data,
  10.                          &cl_info,
  11.                          ClassFileParser::BROADCAST, // publicity level
  12.                          CHECK_NULL);
  13.   // 分配 InstanceKlass 实例,并将构建好的 nonstatic_oop_maps 填充到 InstanceKlass 实例中
  14.   InstanceKlass* result = parser.create_instance_klass(old_stream != stream, *cl_inst_info, CHECK_NULL);
  15.   return result;
  16. }
复制代码
那下面的问题就是如果我们能够知道 _soft_reference_policy 的初始化逻辑,那是不是关于 SoftReference 的一切疑惑就迎刃而解了 ?我们来一起看下 _soft_reference_policy 的初始化过程。
在 ZGC 开始的时间,首先会创建一个 ZDriverGCScope 对象,这里主要进行一些 GC 的准备工作,比如更新 GC 的相关统计信息,设置并行 GC 线程个数,以及本末节的重点,初始化 SoftReference 的处理策略 —— _soft_reference_policy。
  1. ClassFileParser::ClassFileParser(ClassFileStream* stream,
  2.                                  Symbol* name,
  3.                                  ClassLoaderData* loader_data,
  4.                                  const ClassLoadInfo* cl_info,
  5.                                  Publicity pub_level,
  6.                                  TRAPS) :
  7.   ........ 省略 加载,验证,解析逻辑 ......
  8.   // 这里会构建 nonstatic_oop_maps
  9.   post_process_parsed_stream(stream, _cp, CHECK);
  10. }
复制代码
  1. void ClassFileParser::post_process_parsed_stream(
  2.   ..... 省略 .....
  3.   // 对 Java 类中的字段信息进行布局
  4.   _field_info = new FieldLayoutInfo();
  5.   FieldLayoutBuilder lb(class_name(), super_klass(), _cp, _fields,
  6.                         _parsed_annotations->is_contended(), _field_info);
  7.   // 构建 nonstatic_oop_maps
  8.   lb.build_layout();
  9. }
  10. void FieldLayoutBuilder::build_layout() {
  11.   // 在对类中的字段完成布局之后,会调用一个 epilogue() 函数
  12.   compute_regular_layout();
  13. }
  14. // nonstatic_oop_maps的构建逻辑就在这里
  15. void FieldLayoutBuilder::epilogue() {
复制代码
在 JVM 开始初始化 _soft_reference_policy 之前,会调用一个重要的方法 —— should_clear_soft_references,本末节的答案就在这里,该方法就是用来判断,ZGC 是否需要无条件清理 SoftReference 所引用的 referent 对象。

  • 返回 true 表示,在 GC 的过程中只要遇到 SoftReference 对象,那么它引用的 referent 对象就会被当做垃圾清理,SoftReference 对象也会被 JVM 参加到 _reference_pending_list 中等待 ReferenceHandler 线程行止理。这里就和 WeakReference 的语义一样了。
  • 返回 false 表示,内存充足的时间,JVM 就会把 SoftReference 当做平凡的强引用一样处理,它所引用的 referent 对象不会被回收,但内存不敷的时间,被 SoftReference 所引用的  referent 对象就会被回收,SoftReference 也会被参加到 _reference_pending_list 中。
  1. void FieldLayoutBuilder::epilogue() {
  2.   // 开始构建 nonstatic_oop_maps
  3.   int super_oop_map_count = (_super_klass == NULL) ? 0 :_super_klass->nonstatic_oop_map_count();
  4.   int max_oop_map_count = super_oop_map_count + _nonstatic_oopmap_count;
  5.   OopMapBlocksBuilder* nonstatic_oop_maps =
  6.       new OopMapBlocksBuilder(max_oop_map_count);
  7.   // 继承父类的 nonstatic_oop_maps
  8.   if (super_oop_map_count > 0) {
  9.     nonstatic_oop_maps->initialize_inherited_blocks(_super_klass->start_of_nonstatic_oop_maps(),
  10.     _super_klass->nonstatic_oop_map_count());
  11.   }
  12.   // 为非静态成员变量构建 nonstatic_oop_maps
  13.   if (_root_group->oop_fields() != NULL) {
  14.     for (int i = 0; i < _root_group->oop_fields()->length(); i++) {
  15.       LayoutRawBlock* b = _root_group->oop_fields()->at(i);
  16.       // 构建 OopMapBlock,相邻的字段构建在一个 OopMapBlock 中
  17.       // 不相邻的字段分别构建在不同的 OopMapBlock 中
  18.       nonstatic_oop_maps->add(b->offset(), 1);
  19.     }
  20.   }
  21.   // 为 @Contended 标注的非静态成员变量构建 nonstatic_oop_maps
  22.   // 在静态成员变量上标注 @Contended 将会被忽略
  23.   if (!_contended_groups.is_empty()) {
  24.     for (int i = 0; i < _contended_groups.length(); i++) {
  25.       FieldGroup* cg = _contended_groups.at(i);
  26.       if (cg->oop_count() > 0) {
  27.         assert(cg->oop_fields() != NULL && cg->oop_fields()->at(0) != NULL, "oop_count > 0 but no oop fields found");
  28.         // 构建 OopMapBlock,属于同一个 contended_groups 的成员变量在内存中要放在一起
  29.         nonstatic_oop_maps->add(cg->oop_fields()->at(0)->offset(), cg->oop_count());
  30.       }
  31.     }
  32.   }
  33.   // 对相邻的  OopMapBlock 进行排序整理
  34.   // 确保在内存中相邻排列的非静态成员变量被构建在一个 OopMapBlock 中
  35.   nonstatic_oop_maps->compact();
  36.   int nonstatic_field_end = align_up(_layout->last_block()->offset(), heapOopSize);
  37.   // Pass back information needed for InstanceKlass creation
  38.   _info->oop_map_blocks = nonstatic_oop_maps;
  39.   _info->_nonstatic_field_size = (nonstatic_field_end - instanceOopDesc::base_offset_in_bytes()) / heapOopSize;
  40.   _info->_has_nonstatic_fields = _has_nonstatic_fields;
  41. }
复制代码
这里我们看到,在 ZGC 的过程中,只要满足以下三种情况中的恣意一种,那么在 GC 过程中就会无条件地清理 SoftReference 。

  • 引起 GC 的原因是 —— _wb_full_gc ,也就是由 WhiteBox 相关 API 触发的 Full GC,就会无条件清理 SoftReference。
  • 引起 GC 的原因是 —— _metadata_GC_clear_soft_refs,也就是在元数据分配失败的时间触发的 Full GC,元空间内存不敷,情况就很严重了,所以要无条件清理 SoftReference。
  • 引起 GC 的原因是 —— _z_allocation_stall,在 ZGC 接纳壅闭模式分配 Zpage 页面的时间,如果内存不敷无法分配,那么就会触发一次 GC,这时 GC 的触发原因就是 _z_allocation_stall,这种情况下就会无条件清理 SoftReference。
ZGC 非壅闭模式分配 Zpage 的时间如果内存不敷、就直接抛出 OutOfMemoryError,不会启动 GC 。
  1. InstanceKlass* ClassFileParser::create_instance_klass(bool changed_by_loadhook,
  2.                                                       const ClassInstanceInfo& cl_inst_info,
  3.                                                       TRAPS) {
  4.   if (_klass != NULL) {
  5.     return _klass;
  6.   }
  7.   // 分配 InstanceKlass 实例
  8.   InstanceKlass* const ik =
  9.     InstanceKlass::allocate_instance_klass(*this, CHECK_NULL);
  10.   // 填充 InstanceKlass 实例
  11.   fill_instance_klass(ik, changed_by_loadhook, cl_inst_info, CHECK_NULL);
  12.   return ik;
  13. }
复制代码
在我们了解了这个配景之后,在回头来看下 _soft_reference_policy 的初始化过程 :
参数 clear 就是 should_clear_soft_references 函数的返回值
  1. void ClassFileParser::fill_instance_klass(InstanceKlass* ik,
  2.                                           bool changed_by_loadhook,
  3.                                           const ClassInstanceInfo& cl_inst_info,
  4.                                           TRAPS) {
  5.   ik->set_nonstatic_field_size(_field_info->_nonstatic_field_size);
  6.   ik->set_has_nonstatic_fields(_field_info->_has_nonstatic_fields);
  7.   assert(_fac != NULL, "invariant");
  8.   ik->set_static_oop_field_count(_fac->count[STATIC_OOP]);
  9.   // 将构建好的 nonstatic_oop_maps 填充到 InstanceKlass 实例中
  10.   OopMapBlocksBuilder* oop_map_blocks = _field_info->oop_map_blocks;
  11.   if (oop_map_blocks->_nonstatic_oop_map_count > 0) {
  12.     oop_map_blocks->copy(ik->start_of_nonstatic_oop_maps());
  13.   }
  14. }
复制代码
ZGC 接纳了两种策略来处理  SoftReference :

  • always_clear_policy : 当 clear 为 true 的时间,ZGC 就会接纳这种策略,在 GC 的过程中只要遇到 SoftReference,就会无条件回收其引用的 referent 对象,SoftReference 对象也会被 JVM 参加到 _reference_pending_list 中等待 ReferenceHandler 线程行止理。
  • lru_max_heap_policy :当 clear 为 false 的时间,ZGC 就会接纳这种策略,这种情况下 SoftReference 的存活时间取决于 JVM 堆中剩余可用内存的总大小,也是我们下一末节中讨论的重点。
下面我们就来看一下 lru_max_heap_policy 的初始化过程,看看 JVM 是怎样量化内存不敷的 ~~
6.2 JVM 怎样量化内存不敷

LRUMaxHeapPolicy 的 setup() 方法主要用来确定被 SoftReference 所引用的 referent 对象最大的存活时间,这个存活时间是和堆的剩余空间大小有关系的,也就是堆的剩余空间越大 SoftReference 的存活时间就越长,堆的剩余空间越小 SoftReference 的存活时间就越短。
  1. public abstract class Reference<T> {
  2.   private T referent;
  3.   volatile ReferenceQueue<? super T> queue;
  4.   volatile Reference next;
  5.   private transient Reference<?> discovered;
  6. }
复制代码
JVM  首先会获取我们通过  -Xmx 参数指定的最大堆 —— MaxHeapSize,然后在通过 Universe::heap()->used_at_last_gc() 获取上一次 GC 之后 JVM 堆占用的空间,两者相减,就得到了当前 JVM 堆的最大剩余内存空间,并将单位转换为 MB。
现在 JVM 堆的剩余空间我们计算出来了,那怎样根据这个 max_heap 计算 SoftReference 的最大存活时间呢 ?
这里就用到了一个 JVM 参数 —— SoftRefLRUPolicyMSPerMB,我们可以通过 -XX:SoftRefLRUPolicyMSPerMB 来指定,默认为 1000 , 单位为毫秒。
它表达的意思是每 MB 的堆剩余内存空间允许 SoftReference 存活的最大时长,比如当前堆中只剩余 1MB 的内存空间,那么  SoftReference 的最大存活时间就是 1000 ms,如果剩余内存空间为 2MB,那么 SoftReference 的最大存活时间就是 2000 ms 。
现在我们剩余 max_heap 的空间,那么在本轮 GC 中,SoftReference 的最大存活时间就是 —— _max_interval  = max_heap * SoftRefLRUPolicyMSPerMB。
从这里我们可以看出 SoftReference 的最大存活时间 _max_interval,取决于两个因素:

  • 当前 JVM 堆的最大剩余空间。
  • 我们指定的  -XX:SoftRefLRUPolicyMSPerMB 参数值,这个值越大 SoftReference 存活的时间就越久,这个值越小,SoftReference 存活的时间就越短。
在我们得到了这个 _max_interval 之后,那么 JVM 是怎样量化内存不敷呢 ?被 SoftReference 引用的这个 referent 对象到底什么被回收 ?让我们再次回到 JDK 中,来看一下 SoftReference 的实现:
  1. void SystemDictionary::initialize(TRAPS) {
  2.   // Resolve basic classes
  3.   vmClasses::resolve_all(CHECK);
  4. }
  5. void vmClasses::resolve_all(TRAPS) {
  6.     InstanceRefKlass::update_nonstatic_oop_maps(vmClasses::Reference_klass());
  7. }
复制代码
SoftReference 中有两个非常重要的字段,一个是 clock ,另一个是 timestamp。clock 字段是由 JVM 来设置的,在每一次发生 GC 的时间,JVM 都会去更新这个时间戳。具体一点的话,就是在 ZGC 的 Concurrent Process Non-Strong References 阶段处理完所有 Reference 对象之后,JVM 就会来更新这个 clock 字段。
  1. void InstanceRefKlass::update_nonstatic_oop_maps(Klass* k) {
  2.   // Reference 类中的 referent 字段和 discovered 字段的索引偏移从 OopMapBlock 中清除掉
  3.   // 在后面通过 Reference 遍历标记成员变量的时候不需要遍历标记这两个字段
  4.   InstanceKlass* ik = InstanceKlass::cast(k);
  5.   OopMapBlock* map = ik->start_of_nonstatic_oop_maps();
  6.   // Updated map starts at "queue", covers "queue" and "next".
  7.   const int new_offset = java_lang_ref_Reference::queue_offset();
  8.   const unsigned int new_count = 2; // queue and next
  9.    assert(map->offset() == referent_offset, "just checking");
  10.    assert(map->count() == count, "just checking");
  11.    map->set_offset(new_offset);
  12.    map->set_count(new_count);
  13. }
复制代码
在 soft_reference_update_clock()  中 ,JVM 会将 SoftReference 类中的 clock 字段更新为当前时间戳,单位为毫秒。
  1. public abstract class Reference<T> {
  2.   private T referent;
  3.   volatile ReferenceQueue<? super T> queue;
  4.   volatile Reference next;
  5.   private transient Reference<?> discovered;
  6. }
复制代码
而 timestamp 字段用来表示这个 SoftReference 对象有多久没有被访问到了,应用线程越久没有访问 SoftReference,JVM 就越倾向于回收它的 referent 对象。这也是 LRUMaxHeapPolicy 策略中 LRU 的语义表现。
应用线程在每次调用 SoftReference 的 get 方法时间,都会将最近一次的 GC 时间戳 clock 更新到 timestamp 中,如许一来,如果一个 SoftReference 被频仍的访问,那么 clock 和 timestamp 的值不绝是相等的。

如果一个 SoftReference 已经很久没有被访问了,timestamp 就会远远落伍于 clock,因为在没有被访问的这段时间内可能已经发生好频频 GC 了。

在我们了解了这些配景之后,再来看一下 JVM 对于 SoftReference 的回收过程,在本文 5.1 末节中先容的 ZGC Concurrent Mark 阶段中,当 GC 遍历到一个 Reference 范例的对象的时间,会在 should_discover 方法中判断一下这个 Reference 对象所引用的 referent 是否被标记过。如果 referent 没有被标记为 alive , 那么接下来就会将这个 Reference 对象放入 _discovered_list 中,等待后续被 ReferenHandler 处理,referent 也会在本轮 GC 中被回收掉。
  1. // zReferenceProcessor.cpp 文件
  2. OopHandle Universe::_reference_pending_list;
  3. // Create a handle for reference_pending_list
  4. _reference_pending_list = OopHandle(vm_global(), NULL);
复制代码
如果当前遍历到的 Reference 对象是 SoftReference 范例的,那么就需要在 is_softly_live 方法中根据前面先容的 LRUMaxHeapPolicy 来判断这个 SoftReference 引用的 referent 对象是否满足存活的条件。
  1. void ZDriver::gc(const ZDriverRequest& request) {
  2.   ZDriverGCScope scope(request);
  3.   // Phase 1: Pause Mark Start
  4.   // 初始化 gc 相关的统计信息,清空 object alocator 的缓存页,切换地址视图,设置标记条带个数
  5.   pause_mark_start();
  6.   // Phase 2: Concurrent Mark
  7.   // 标记 gc root, 标记普通对象,以及 Reference 对象
  8.   // 经过主动刷新,被动刷新之后,如果标记栈中还有对象,也不会再进行标记了
  9.   // 剩下的对象标记任务放到 pause_mark_end 中 STW 阶段执行
  10.   concurrent(mark);
  11.   // Phase 3: Pause Mark End 再标记阶段,标记上一阶段剩下的对象
  12.   // zgc 低延迟的精髓,如果 1ms 内结束不了 STW 标记,那么就在发起一轮 concurrent 标记
  13.   // 目的是降低应用线程的停顿控制在 1ms 以内
  14.   while (!pause_mark_end()) {
  15.     // 1ms 内没有标记完应用线程本地标记栈的内容,那么就重新开始一轮并发标记。
  16.     // Phase 3.5: Concurrent Mark Continue
  17.     concurrent(mark_continue);
  18.   }
  19.   // Phase 4: Concurrent Mark Free
  20.   // 释放标记栈资源
  21.   concurrent(mark_free);
  22.   // Phase 5: Concurrent Process Non-Strong References
  23.   // 这里就是本小节讨论的重点
  24.   concurrent(process_non_strong_references);
  25.   ....... 省略 .......
  26. }
复制代码
通过 java_lang_ref_SoftReference::clock() 获取到的就是前面先容的 SoftReference.clock  字段 —— timestamp_clock。
通过 java_lang_ref_SoftReference::timestamp(p) 获取到的就是前面先容的 SoftReference.timestamp  字段。
如果 SoftReference.clock 与 SoftReference.timestamp  的差值 —— interval,小于即是前面先容的 SoftReference 最大存活时间 —— _max_interval,那么这个 SoftReference 所引用的 referent 对象在本轮 GC 中就不会被回收,SoftReference 对象也不会被放到 _reference_pending_list 中被 ReferenceHandler 线程处理。
  1. class ZReferenceProcessor : public ReferenceDiscoverer {
  2.   ZPerWorker<oop>      _discovered_list;
  3.   ZContended<oop>      _pending_list;
  4. }
复制代码
我们看到,在 JVM 创建对象实例的时间,会首先通过 has_finalizer() 方法判断这个 Java 类有没有重写 finalize() 方法,如果重写了就会调用 register_finalizer 方法,JVM 终极会调用 JDK 中的 Finalizer 类的静态方法 register。
  1. void ZMark::follow_object(oop obj, bool finalizable) {
  2.   if (finalizable) {
  3.     ZMarkBarrierOopClosure<true /* finalizable */> cl;
  4.     obj->oop_iterate(&cl);
  5.   } else {
  6.     // 最终的标记逻辑是在这个闭包中完成的
  7.     ZMarkBarrierOopClosure<false /* finalizable */> cl;
  8.     // 遍历标记 obj 的所有非静态成员变量
  9.     obj->oop_iterate(&cl);
  10.   }
  11. }
复制代码
在这里 JVM 会将刚刚创建出来的平凡 Java 对象 —— finalizee,与一个 Finalizer 对象关联起来, Finalizer 对象的范例正是 FinalReference 。这里我们可以看到,当一个 Java 类重写了 finalize() 方法的时间,每当创建一个该类的实例对象,JVM 就会自动创建一个对应的 Finalizer 对象
Finalizer 的整体设计和之前先容的 Cleaner 非常相似,差别的是 Cleaner 是一个 PhantomReference,而 Finalizer 是一个 FinalReference。
它们都有一个 ReferenceQueue,只不过 Cleaner 中的那个基本没啥用,但是 Finalizer 中的这个 ReferenceQueue 却有非常重要的作用。
它们内部都有一个双向链表,里面包含了 JVM 堆中所有的 Finalizer 对象,用来确保这些 Finalizer 在执行 finalizee 对象的 finalize() 方法之前不会被 GC 回收掉。
  1. template <typename OopClosureType>
  2. void oopDesc::oop_iterate(OopClosureType* cl) {
  3.   OopIteratorClosureDispatch::oop_oop_iterate(cl, this, klass());
  4. }
复制代码
在创建 Finalizer 对象的时间,首先会调用父类方法,将被引用的 Java 对象以及 ReferenceQueue 关联注册到 FinalReference 中。
[code]    Reference(T referent, ReferenceQueue

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

络腮胡菲菲

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表