一、引用概览
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 WeakHashMap
-
WeakHashMap
线程不安全, 通过
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()
方法;
- FinalReference 唯一的子类,用于执行 重写的
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) { }// 解除 FinalReferencesuper.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
可以根治;