细说Java引用(强、软、弱、虚)和GC流程(一)
- 创业
- 2025-08-28 18:03:01

一、引用概览 1.1 引用简介
JDK1.2中引入了 Reference 抽象类及其子类,来满足不同场景的 JVM 垃圾回收工作:
SoftReference
内存不足,GC发生时,引用的对象(没有强引用时)会被清理;高速缓存使用,内存不够,就回收,如需要读取大量的本地图片;WeakReference
GC发生时,引用的对象(没有强引用 和 软引用 时)会被清理;WeakHashMap、ThreadLocalMap 中 key 均是弱引用;jdk动态代理中缓存代理类的WeakCache;PhantomReference
GC发生时,引用的对象(没有强引用 和 软引用时)会被清理;本质是用来跟踪被引用对象是否被 GC 回收;堆外内存(DirectByteBuffer )清理;在静态内部类中,经常会使用虚引用。例如:一个类发送网络请求,承担 callback 的静态内部类,则常以虚引用的方式来保存外部类的引用,当外部类需要被 JVM 回收时,不会因为网络请求没有及时回应,引起内存泄漏;MySQL使用虚引用来解决IO资源回收问题;虚引用往往作为一种兜底策略,避免用户忘记释放资源,引发内存泄露FinalReference
由其唯一子类Finalizer来处理重写了 Object.finalize() 方法的实例对象,jdk 1.9 已经废弃该方法;因为必须执行重写了 Object.finalize() 的方法后才能执行GC回收实例对象,会拖慢GC速度,finalize()中操作耗时的话,GC基本无法进行;前面三种引用我们编码时可以使用,也是我们所熟知的软引用、弱引用和虚引用,FinalReference 是由 JVM 使用的,下面图片显示了FinalReference 类修饰符并非public。
除此而外,平常编程使用 new 创建的对象均为强引用。
1.2 引用内存分布 1.2.1 编码示例 虚引用创建时必须传入引用队列(ReferenceQueue),软引用和弱引用可以不传;通过Reference.get() 获取引用的对象时,虚引用永远返回 null; 1.2.2 内存分布 1.3 引用使用流程 1.3.1 引用生命周期 正常使用 0、创建对象,图示{1}; 1、创建引用队列,创建引用对象,图示{2}; 2、使用完毕后,{1} 会断开,对象没有强引用了;引用清理介入 3、GC清理对象时,发现有引用(软、弱、虚、FinalReference); 3.0、引用为FinalReference 时,执行步骤 4; 3.1、内存不足,清理对象实例;内存充足时,清理弱引用和虚引用所引用的对象实例; 3.2、如果对象实例被清理,继续往下走,否则终止; 4、GC线程将引用对象添加到 pending 队列中,同时唤醒阻塞的 ReferenceHandler 线程; 5、ReferenceHandler 线程消费 pending 队列; 6、消费后的引用对象添加到用户传入的引用队列中; 6.1、如果创建引用时没有传入引用队列就终止; 7、用户线程消费引用队列。 ReferenceHandler 线程Reference 类静态加载 ReferenceHandler 线程:优先级最高的守护线程
1.3.2 引用状态流转 1.4 GC 引用 1.4.1 引用处理原码 void ReferenceProcessor::process_discovered_references( BoolObjectClosure* is_alive, OopClosure* keep_alive, VoidClosure* complete_gc, AbstractRefProcTaskExecutor* task_executor) { NOT_PRODUCT(verify_ok_to_handle_reflists()); assert(!enqueuing_is_done(), "If here enqueuing should not be complete"); // Stop treating discovered references specially. disable_discovery(); bool trace_time = PrintGCDetails && PrintReferenceGC; // Soft references { TraceTime tt("SoftReference", trace_time, false, gclog_or_tty); process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true, is_alive, keep_alive, complete_gc, task_executor); } update_soft_ref_master_clock(); // Weak references { TraceTime tt("WeakReference", trace_time, false, gclog_or_tty); process_discovered_reflist(_discoveredWeakRefs, NULL, true, is_alive, keep_alive, complete_gc, task_executor); } // Final references { TraceTime tt("FinalReference", trace_time, false, gclog_or_tty); process_discovered_reflist(_discoveredFinalRefs, NULL, false, is_alive, keep_alive, complete_gc, task_executor); } // Phantom references { TraceTime tt("PhantomReference", trace_time, false, gclog_or_tty); process_discovered_reflist(_discoveredPhantomRefs, NULL, false, is_alive, keep_alive, complete_gc, task_executor); } // Weak global JNI references. It would make more sense (semantically) to // traverse these simultaneously with the regular weak references above, but // that is not how the JDK1.2 specification is. See #4126360. Native code can // thus use JNI weak references to circumvent the phantom references and // resurrect a "post-mortem" object. { TraceTime tt("JNI Weak Reference", trace_time, false, gclog_or_tty); if (task_executor != NULL) { task_executor->set_single_threaded_mode(); } process_phaseJNI(is_alive, keep_alive, complete_gc); } } 1.4.2 引用GC日志增加JVM启动参数: -XX:+PrintReferenceGC -XX:+PrintGCDetails, 打印各种引用对象的详细回收时间。
涉及系统存在大量引用回收时,GC耗时显著增加,可以通过增加参数 -XX:+ParallelRefProcEnabled 开启 ( JDK8版本默认关闭的,在JDK9+之后默认开启 ) 并行处理引用来快速优化GC,具体根因后面可以继续分析。
日志样例如下: 2023-06-04T10:28:52.886+0800: 24397.548: [GC concurrent-root-region-scan-start]:开始扫描并发根区域。 2023-06-04T10:28:52.941+0800: 24397.602: [GC concurrent-root-region-scan-end, 0.0545027 secs]:并发根区域扫描结束,持续时间为0.0545027秒。 2023-06-04T10:28:52.941+0800: 24397.602: [GC concurrent-mark-start]:开始并发标记过程。 2023-06-04T10:28:53.198+0800: 24397.859: [GC concurrent-mark-end, 0.2565503 secs]:并发标记过程结束,持续时间为0.2565503秒。 2023-06-04T10:28:53.199+0800: 24397.860: [GC remark]: G1执行remark阶段。 2023-06-04T10:28:53.199+0800: 24397.860: [Finalize Marking, 0.0004169 secs]:标记finalize队列中待处理对象,持续时间为0.0004169秒。 2023-06-04T10:28:53.199+0800: 24397.861: [GC ref-proc]: 进行引用处理。 2023-06-04T10:28:53.199+0800: 24397.861: [SoftReference, 9247 refs, 0.0035753 secs]:处理软引用,持续时间为0.0035753秒。 2023-06-04T10:28:53.203+0800: 24397.864: [WeakReference, 963 refs, 0.0003121 secs]:处理弱引用,持续时间为0.0003121秒。 2023-06-04T10:28:53.203+0800: 24397.865: [FinalReference, 60971 refs, 0.0693649 secs]:处理虚引用,持续时间为0.0693649秒。 2023-06-04T10:28:53.273+0800: 24397.934: [PhantomReference, 49828 refs, 20 refs, 4.5339260 secs]:处理final reference中的phantom引用,持续时间为4.5339260秒。 2023-06-04T10:28:57.807+0800: 24402.468: [JNI Weak Reference, 0.0000755 secs]:处理JNI weak引用,持续时间为0.0000755秒。 2023-06-04T10:28:57.821+0800: 24402.482: [Unloading, 0.0332897 secs]:卸载无用的类,持续时间为0.0332897秒。 [Times: user=4.60 sys=0.31, real=4.67 secs]:垃圾回收的时间信息,user表示用户态CPU时间、sys表示内核态CPU时间、real表示实际运行时间。 2023-06-04T10:28:57.863+0800: 24402.524: [GC cleanup 4850M->4850M(9984M), 0.0031413 secs]:执行cleanup操作,将堆大小从4850M调整为4850M,持续时间为0.0031413秒。 1.5 案例分析 1.5.0 高速缓存 1.5.1 WeakHashMapWeakHashMap
线程不安全, 通过 Collections.synchronizedMap来生成一个线程安全的map
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> { //…… Entry<K,V>[] table; /** * Reference queue for cleared WeakEntries */ private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); }WeakHashMap.Entry
1、弱引用对象; 2、传递了引用队列; 3、没有引用队列消费线程, 通过WeakHashMap中大多数方法(get(), put(),size()等)来消费引用队列,从而释放Entry;
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; final int hash; Entry<K,V> next; /** * Creates new entry. */ Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) { super(key, queue); this.value = value; this.hash = hash; this.next = next; } 1.5.1.2 WeakhashMap使用场景 缓存系统:Tomcat的工具类里的 ConcurrentCache package org.apache.tomcat.util.collections; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; public final class ConcurrentCache<K,V> { private final int size; private final Map<K,V> eden; private final Map<K,V> longterm; public ConcurrentCache(int size) { this.size = size; this.eden = new ConcurrentHashMap<>(size); this.longterm = new WeakHashMap<>(size); } public V get(K k) { V v = this.eden.get(k); if (v == null) { synchronized (longterm) { v = this.longterm.get(k); } if (v != null) { this.eden.put(k, v); } } return v; } public void put(K k, V v) { if (this.eden.size() >= size) { synchronized (longterm) { this.longterm.putAll(this.eden); } this.eden.clear(); } this.eden.put(k, v); } } 诊断工具:在阿里开源的Java诊断工具Arthas中使用了WeakHashMap做类-字节码的缓存。 /** * 类-字节码缓存 * Class: Class * byte[]: bytes of Class **/ private final static Map<Class<?>, byte[]> classBytesCache = new WeakHashMap<>(); 1.5.2 ThreadLocal 1.5.2.1 ThreadLocal 内存分配图如下所示 1.5.2.2 ThreadLocal 涉及对象及垃圾回收过程ThreadLocalMap
1、ThreadLocalMap 是 Thread 内部的成员变量,即图示{0}为强引用; 2、ThreadLocalMap 是一个map,key是 WeakReference<ThreadLocal<?>>,即 key 是一个弱引用对象,图示{2};value 是一个强引用,图示{3}; 3、当Thread 销毁之后对应的 ThreadLocalMap 也就随之销毁;
ThreadLocalMap.Entry
1、弱引用对象; 2、没有传递引用队列;
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }ThreadLocal
正常使用 0、创建ThreadLocal对象,即图示{1}; 1、通过方法set(obj) 时会将值 obj 填充进当前线程的ThreadLocalMap,即图示{2}、{3};弱引用清理介入 2、使用完毕后,{1} 断开; 3、GC时发现ThreadLocal对象只存在弱引用,可以直接回收ThreadLocal对象; 1.5.2.3 ThreadLocal 思考ThreadLocalMap 的 key 是强引用不可以吗?
答:当线程一直存活时(实际使用时会用线程池,线程大概率不会销毁),图示{2}为强引用时,垃圾ThreadLocal对象无法回收,造成内存泄漏;
ThreadLocalMap 的 value 为啥不设计为弱引用?
答:当 value 只有弱引用时,GC时会直接回收value,当使用key来获取value时,value返回null,这显然有问题;
ThreadLocalMap 什么情况下会导致导致内存泄漏?
答:当线程一直存活时,key 因为是弱引用,GC会回收,但ThreadLocalMap的 value 却是强引用,会阻止GC回收;
时间一长,ThreadLocalMap会存在很多 key 为 null,value 不为 null的情况; 这种情况调用ThreadLocal.get(),ThreadLocal.set(),ThreadLocal.remove() 时 有概率(遇到hash冲突) 将之前的 key 为 null 的 entry 清理;
所以,使用完毕ThreadLocal,一定要记得执行remove()方法。
综上,使用完ThreadLocal,Thread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocal调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
ThreadLocal 的正确使用姿势
定义为类的静态变量 private static final ThreadLocal<Integer>,如官方文档所示,通过静态方法ThreadId.get() 来使用,这样使得 key 永远存活(ThreadLocal实例在内存只有一份);使用完毕后通过实例方法ThreadLocal.remove() 来移除 Entry,从而避免ThreadLocalMap中的value 产生内存泄漏。ThreadLocalMap 内部Hash冲突使用的是线性探测,并非HashMap的拉链法。
HashMap 是性能优先,尽可能的保证元素的高效访问。ThreadLocalMap 性能不是第一要素;如果数组元素比较密集的话,ThreadLocalMap 不管是 set 还是 get 都会不可避免地扫描很多节点,这肯定会影响性能。但是换来的收益,就是 ThreadLocalMap 可以在扫描节点时主动发现过期节点(key 为 null )且清理掉,尽可能的避免内存泄漏。
ThreadLocal 中一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是斐波那契数 也叫 黄金分割数。hash增量为这个数字,使得 hash 码能均匀的分布在2的N次方的数组里, 即 Entry[] table,所以ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突;
ThreadLocal 往往存放的数据量不会特别大,而且 key 是弱引用又会被垃圾回收,所以,线性探测法会查询更快,同时也更省空间。
1.5.2.4 ThreadLocal 使用场景HikariPool 数据库连接池高性能原因之一:
连接池从连接池中获取连接时对于同一个线程在ThreadLocal中添加了缓存,同一线程获取连接时没有并发操作。
全局 Token 管理:自定义拦截器,把Token放入ThreadLocal 后续通过ThreadLocal ,获取用户信息;
org.slf4j.MDC(Mapped Diagnostic Context) 去埋点TraceId,跟踪多个服务调用,巧妙实现链路跟踪(MDC 底层依赖ThreadLocal);
不同层数据库连接读取,用于完成一个事务;
ThreadLocal 用于同一个线程内,对于父子线程使用InheritableThreadLocal,线程池使用阿里巴巴的TransmittableThreadLocal组件
1.5.3 堆外内存(DirectByteBuffer )回收HeapByteBuffer 在堆内存分配;
ByteBuffer.allocate();
DirectByteBuffer 在堆外分配;
ByteBuffer.allocateDirect();
1.5.3.1 DirectByteBuffer 内存分配图如下所示 1.5.3.2 DirectByteBuffer 涉及对象及堆外内存释放过程Deallocator
0、是一个 Runnable 对象; 1、记录了堆外内存地址、大小; 2、负责清理堆外内存。
Cleaner
0、是一个虚引用(PhantomReference)对象 1、ReferenceHandler 线程(Reference 类加载时会创建)触发清理; 2、内部有 前驱和后继,方便组成双向链表(图示线路{5}),链表头 first 是Cleaner 类的静态变量;避免在 DirectByteBuffer 对象前被GC; 3、线程安全。
DirectByteBuffer
正常使用 0、记录了堆外内存地址、大小; 1、正常使用时通过图示线路{1}、{2}读写堆外内存; 2、使用完毕后,{1} 会断开,变成不可达对象;虚引用清理介入 3、GC 发现 DirectByteBuffer 对象有虚引用{4},同时清理 DirectByteBuffer 对象,断开{3},{4}; 4、GC 线程将虚引用对象 Cleaner 放入到 pending 队列,同时唤醒ReferenceHandler线程; 5、ReferenceHandler 线程消费 pending 队列,拿到 Cleaner 对象后,调用Cleaner.clean()方法; 6、Cleaner.clean()首先断开{5},然后内部调用 Deallocator.run() 方法清理堆外内存(依赖unsafe.freeMemory()); 7、Cleaner 对象和Deallocator 在后续GC时可以回收; ReferenceHandler 线程 优先级最高(可查阅 1.3.1 引用生命周期处代码片段),只要GC触发后,就可以释放堆外内存;尴尬的是,GC时机不确定,那堆外内存释放的时间也不确定了?不怕,下一次分配堆外内存时,发现内存不足,会触发System.gc();所以,下一次分配是啥时候呢?不确定,哈哈。 1.5.4 FinalReference 回收 1.5.4.1 FinalReference 内存分配图如下所示Finalizer
FinalReference 唯一的子类,用于执行 重写的java.lang.Object.finalize()方法; 1.5.4.2 Finalizer 内存释放过程正常创建对象
1、创建对象(该对象覆写了方法:Object.finalize() ,记为FinalReferenceObj),图示{1}; 2、JVM 将该对象注册到Finalizer上,即创建 FinalReference 引用的对象 Finalizer,引用指向之前创建的对象,图示{2}; 3、同时,将Finalizer 实例对象加入到 unfinalized 链表中, 图示{7};
FinalReference 引用介入释放
4、FinalReferenceObj 使用完毕,断开{1}; 5、GC时发现对象FinalReferenceObj有FinalReference引用,暂停回收FinalReferenceObj对象; 6、gc线程将Finalizer 对象放入 pending 队列; 7、ReferenceHandler线程消费 pending 队列,取出Finalizer 对象,加入到引用队列中; 8、FinalizerThread线程消费引用队列,取出Finalizer 对象,找到FinalReferenceObj 对象,执行其覆写的方法:Object.finalize() ; 9、断开 FinalReference 引用,图示{2},后续GC可以回收FinalReferenceObj 对象; 10、从 unfinalized 链表移除Finalizer 对象,图示{7};后续GC可以回收Finalizer 对象;
FinalizerThread线程 优先级较低(可以查阅1.5.4.3 Finalizer代码简析 ),所以执行 finalize() 方法会延迟(如果finalize()方法内部也有耗时操作,那就是雪上加霜了),导致最终累积大量垃圾,造成GC耗时,拖垮系统。
1.5.4.3 Finalizer代码简析 final class Finalizer extends FinalReference<Object> { // …… // 1.0 unfinalized实际上是一个双向链表,在add方法被调用后,就会将当前对象加入到unfinalized链表。 // 2.0 当前创建对象在虚拟机内仅该unfinalized链表持有一份引用 // 3.0 当执行完重写的java.lang.Object#finalize方法后,才会重列表移除;避免FinalReference 引用在实例对象前被GC; // 这里会拖垮GC效率,发现GC完了一次,压根没有释放内存 // 4.0 本质就是给GC的对象一个强引用 private static Finalizer unfinalized = null; private Finalizer next, prev; private Finalizer(Object finalizee) { super(finalizee, queue); add(); // 将当前对象加入到unfinalized链表。 } /** * register方法仅会被虚拟机所调用,而且,只有重写了java.lang.Object#finalize方法的类才会被作为参数调用Finalizer#register方法。 **/ /* Invoked by VM */ static void register(Object finalizee) { new Finalizer(finalizee); } // 启动 FinalizerThread 线程消费引用队列里的FinalReference, // 就是执行重写的java.lang.Object#finalize方法,然后从unfinalized 队列中移除,方便 GC。 static { // …… Thread finalizer = new FinalizerThread(tg); // 该线程的优先级并不能保证 finalizer.setPriority(Thread.MAX_PRIORITY - 2); finalizer.setDaemon(true); finalizer.start(); } // 自定义引用队列 private static ReferenceQueue<Object> queue = new ReferenceQueue<>(); // …… /** * 执行obj.finalize()方法 **/ private void runFinalizer(JavaLangAccess jla) { // …… try { // 拿到覆写finalize()方法的对象,再次建立强引用 Object finalizee = this.get(); assert finalizee != null; if (!(finalizee instanceof java.lang.Enum)) { // 执行obj.finalize()方法 jla.invokeFinalize(finalizee); // 将刚刚的强引用释放; finalizee = null; } } catch (Throwable x) { } // 解除 FinalReference super.clear(); } // …… } 1.5.4.3 Java 程序启动时的线程java程序启动时就有 finalizer 线程(FinalizerThread)、ReferenceHandler 线程,如下图示:
二、引用堆积引发GC耗时血案RPC 使用短连接调用,导致 Socket 的 FinalReference 引用较多,致使 YoungGC 耗时较长
rpc项目中的长连接与短连接的思考解决: 1、增加参数-XX:+ParallelRefProcEnabled 可以缓解; 2、通过将短连接改成长连接,减少了 Socket 对象的创建,从而减少 FinalReference,来降低 YoungGC 耗时。
Mysql连接断开兜底策略使用ConnectionPhantomReference,导致大量PhantomReference 堆积,引起GC耗时严重
案例一案例二案例三案例四解决: 1、增加参数-XX:+ParallelRefProcEnabled 可以缓解; 2、升级MySQL jdbc driver到8.0.22+,开启disableAbandonedConnectionCleanup 可以根治;
细说Java引用(强、软、弱、虚)和GC流程(一)由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“细说Java引用(强、软、弱、虚)和GC流程(一)”