背景
XOP服务运行期间,查看Grafana面板,发现堆内存周期性堆积,Full GC时间略长,需要调查下原因
目录
- 垃圾收集器概述
- 常见的垃圾收集器
- 分区收集策略
- 为什么CMS没成为默认收集器
- 查看JVM运行时环境
- 分析快照
- PhantomReference虚引用
1、垃圾收集器概述
常见的垃圾收集器
按照收集策略划分
- 新生代收集器:Serial、ParNew、Parallel Scavenge;
- 老年代收集器:Serial Old、Parallel Old、CMS;
- 整堆分区收集器:G1、ZGC、Shenandoah
吞吐量优先、停顿时间优先
- 吞吐量优先:Parallel Scavenge收集器、Parallel Old 收集器。
- 停顿时间优先:CMS(Concurrent Mark-Sweep)收集器。
吞吐量与停顿时间适用场景
- 吞吐量优先:交互少,计算多,适合在后台运算的场景。
- 停顿时间优先:交互多,对响应速度要求高
串行、并行、并发
- 串行:Serial、Serial Old,垃圾回收必须暂停全部工作线程,无法利用多核优势。
- 并行:ParNew、Parallel Scavenge、Parallel Old,并行描述的是多条垃圾收集器线程之前的关系,说明同一时间有多条垃圾收集器线程在工作,此时用户线程默认是处于等待状态。
- 并发:CMS、G1,并发描述的是垃圾收集器线程和用户线程之间的关系
算法,参考往期博客
- 复制算法:Serial、ParNew、Parallel Scavenge、G1
- 标记-清除:CMS
- 标记-整理:Serial Old、Parallel Old、G1
通过参数选择需要使用的垃圾收集器
-XX:+UseSerialGC
,虚拟机运行在Client模式下的默认值,Serial+Serial Old。-XX:+UseParNewGC
,ParNew+Serial Old,在JDK1.8被废弃,在JDK1.7还可以使用。-XX:+UseConcMarkSweepGC
,ParNew+CMS+Serial Old。-XX:+UseParallelGC
,虚拟机运行在Server模式下的默认值,Parallel Scavenge+Serial Old(PS Mark Sweep)。-XX:+UseParallelOldGC
,Parallel Scavenge+Parallel Old。-XX:+UseG1GC
,G1+G1。
分区收集策略
JDK7/8使用采用分代收集比较多的垃圾收集器组合
另外随着JDK版本更新,JDK9之后默认的垃圾收集器为G1,之前分代收集思想逐渐被分区收集思想代替,考虑的是收集堆内存的哪个部分才能获得收益最大,如G1、ZGC-JDK15开始准备好生产了、Shenandoah,并且随着JDK的版本的升级吞吐量、响应速度都在不断优化提升。
- G1:开创了垃圾收集器面向局部收集的设计思路 和 基于Region的内存布局形式。不再像之前那样划代,而是把连续的堆内存划分为一块块的Region,每一个Region都可以根据需要充当之前分代区域的Eden、Survivor、老年代空间,除此之外还有一类特殊的Humongous区域专门用于存储大对象。它可以面向堆内存任何部分来组成回收集(Collection Set),衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,优先处理回收收益最大的那些 Region,这也就是 Garbage First 名字的由来。
- Shennandoah:一款RedHat独立开发后来贡献给OpenJDK的收集器,在OracleJDK不存在,目标之一是暂停时间与堆大小无关,并且经过优化,中断时间不超过几毫秒。
- ZGC:目标和Shennandoah类似,希望在对吞吐量影响不大的情况下(相比G1应用程序吞吐量减少不超过15%),实现任意堆内存大小都可以吧垃圾收集器停顿时间限制在十毫秒内。
深入学习参考
- GC - Java 垃圾回收器之G1详解
- Java Hotspot G1 GC的一些关键技术-美团技术团队
- 新一代垃圾回收器ZGC的探索与实践-美团技术团队
为什么CMS从来没成为默认收集器
CMS(Concurrent Mark Sweep)是一种 以获取最短回收停顿时间为目标 的收集器。
在 JDK 5 发布时,HotSpot 虚拟机推出了一款在强交互应用中具有划时代意义的垃圾收集器——CMS 收集器。这款收集器是 HotSpot 中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS 比 G1 早不了多少。CMS 从 JDK 5 开始加入,6 成熟;而 G1 是 7 加入,8 成熟,9 正式成为默认 GC 策略。此时 CMS 就被标记为 Deprecated,随后在 JDK 14 中被移除。
CMS并不是一个非常成功的GC策略,GC优化一般考虑点是吞吐量和响应时间,而CMS
- 采用标记-清除算法,当处理器核比较少的时候,会造成比较大的负载,而且容易产生内存碎片,碎片太多无能为力的时候触发Concurrent Mode Failure还需要Serial Old来擦屁股。
- 仅针对老年代,还需要一个新生代收集器,但是和Parallel Scavenage又不兼容,只能选择性能不如Parallel Scavenage的PerNew。
- 需要调整的参数比较多,比G1多一倍
以上的种种,造成的结果就是 ParNew + CMS + Serial Old 的组合工作起来其实并不稳定。为了得到 CMS 那一点好处,需要付出很多的代价(包括 JVM 调参)。
CMS 相比前辈们,没有带来革命性的改变;而它的后辈们比它强太多。它自身的实现又很复杂,兼容性又差,调参也很麻烦,所以无法成为默认 GC 方案了。
参考
- Java——七种垃圾收集器+JDK11最新ZGC
- 面试:JVM 垃圾回收器-腾讯云开发者社区-腾讯云
- Java 8 vs Java 17 垃圾收集器
2、查看JVM运行时环境
数据库环境是MySQL,连接池使用的是HikariPool,驱动是mysql-connector-java-8.0.21.jar,当前生产环境JVM运行参数
root 14968 1 0 2月23 ? 00:41:51 java -server -XX:MetaspaceSize=160m
-XX:MaxMetaspaceSize=160m
-Xms1024m
-Xmx1024m
-Xss256k
-Duser.timezone=GMT+08
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/serviceroot/xkw-xopqbm-api-service/xkw-xopqbm-api-service.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/serviceroot/xkw-xopqbm-api-service/logs/xkw-xopqbm-api-service-gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=1m -jar /data/serviceroot/xkw-xopqbm-api-service/xkw-xopqbm-api-service.jar
--spring.profiles.active=test
--server.port=9501
-Dons.client.logLevel=ERROR
当前服务器的java版本为1.8
[root@localmdmtest ~]# java -version
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)
查看使用的垃圾收集器 jmap -heap pid
,可以看到当前使用的垃圾收集器 ParNew+CMS+Serial Old
[root@localmdmtest ~]# jmap -heap 14968Attaching to process ID 14968, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.281-b09using parallel threads in the new generation.
using thread-local object allocation.# Concurrent Mark-Sweep GC :CMS回收器
# Mark Sweep Compact GC: 串行GC(Serial GC)
# Parallel GC with 2 thread(s): 并行GC(ParNew)# 这里看出是CMS
Concurrent Mark-Sweep GCHeap Configuration:MinHeapFreeRatio = 40MaxHeapFreeRatio = 70MaxHeapSize = 1073741824 (1024.0MB)NewSize = 357892096 (341.3125MB)MaxNewSize = 357892096 (341.3125MB)OldSize = 715849728 (682.6875MB)NewRatio = 2SurvivorRatio = 8MetaspaceSize = 167772160 (160.0MB)CompressedClassSpaceSize = 159383552 (152.0MB)MaxMetaspaceSize = 167772160 (160.0MB)G1HeapRegionSize = 0 (0.0MB)Heap Usage:
New Generation (Eden + 1 Survivor Space):capacity = 322109440 (307.1875MB)used = 245758352 (234.37342834472656MB)free = 76351088 (72.81407165527344MB)76.2965382200534% used
Eden Space:capacity = 286326784 (273.0625MB)used = 239080888 (228.00530242919922MB)free = 47245896 (45.05719757080078MB)83.49930965592098% used
From Space:capacity = 35782656 (34.125MB)used = 6677464 (6.368125915527344MB)free = 29105192 (27.756874084472656MB)18.66117484403617% used
To Space:capacity = 35782656 (34.125MB)used = 0 (0.0MB)free = 35782656 (34.125MB)0.0% used
concurrent mark-sweep generation:capacity = 715849728 (682.6875MB)used = 84539240 (80.6229019165039MB)free = 631310488 (602.0645980834961MB)11.809634996466745% used45864 interned Strings occupying 4725904 bytes.
3、分析快照
dump快照命令:jmap -dump:live,format=b,file=/home/scl/xopqbm/heapdump.hprof xxx
,可以使用MAT或者在线工具https://heaphero.io/分析快照
发现11,319 instances of "com.mysql.cj.jdbc.AbandonedConnectionCleanupThread$ConnectionFinalizerPhantomReference"
也就是ConnectionFinalizerPhantomReference占了80%的堆内存,为什么会这么多对象,需要分析下原因。
PhantomReference虚引用
ConnectionFinalizerPhantomReference这个类在AbandonedConnectionCleanupThread类内定义,继承PhantomReference
// AbandonedConnectionCleanupThread类内
private static class ConnectionFinalizerPhantomReference extends PhantomReference<MysqlConnection> {private NetworkResources networkResources;ConnectionFinalizerPhantomReference(MysqlConnection conn, NetworkResources networkResources, ReferenceQueue<? super MysqlConnection> refQueue) {super(conn, refQueue);this.networkResources = networkResources;}void finalizeResources() {if (this.networkResources != null) {try {this.networkResources.forceClose();} finally {this.networkResources = null;}}}}
对于PhantomReference虚引用的概念,简单就是他可以将某个对象标记为虚的,一般用于标记对象是否被GC回收,虚引用也称为“幽灵引用”,它是最弱的一种引用关系。
- 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
- 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
- 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。
查看AbandonedConnectionCleanupThread类内部的属性和主要的run方法,主要的属性有
- Set connectionFinalizerPhantomRefs
- ReferenceQueue referenceQueue
private static final Set<ConnectionFinalizerPhantomReference> connectionFinalizerPhantomRefs = ConcurrentHashMap.newKeySet();private static final ReferenceQueue<MysqlConnection> referenceQueue = new ReferenceQueue<>();private static final ExecutorService cleanupThreadExcecutorService;private static Thread threadRef = null;private static Lock threadRefLock = new ReentrantLock();
public void run() {for (;;) {try {checkThreadContextClassLoader();Reference<? extends MysqlConnection> reference = referenceQueue.remove(5000);if (reference != null) {finalizeResource((ConnectionFinalizerPhantomReference) reference);}} catch (InterruptedException e) {threadRefLock.lock();try {threadRef = null;// Finalize remaining references.Reference<? extends MysqlConnection> reference;while ((reference = referenceQueue.poll()) != null) {finalizeResource((ConnectionFinalizerPhantomReference) reference);}connectionFinalizerPhantomRefs.clear();} finally {threadRefLock.unlock();}return;} catch (Exception ex) {// Nowhere to really log this.}}
}
// TODO
参考
- JVM 优化踩坑记
- 数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
- PhantomReference 引发的GC问题-CSDN博客
- AbandonedConnectionCleanupThread$ConnectionFinalizerPhantomReference内存溢出_abandoned connection cleanup thread-CSDN博客
- G1垃圾回收参数调优及MySQL虚引用造成GC时间过长分析 | 京东云技术团队 - 墨天轮