常见垃圾回收器组合参数设定:(JDK1.8)
-
-XX:+UseSerialGC= Serial New (DefNew) + Serial Old 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器 -
-XX:+UseParNewGC= ParNew + SerialOld 这个组合已经很少用(在某些版本中已经废弃) -
-XX:+UseConcMarkSweepGC= ParNew + CMS + Serial Old -
-XX:+UseParallelGC= Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】 -
-XX:+UseParallelOldGC= Parallel Scavenge + Parallel Old -
-XX:+UseG1GC= G1
了解JVM常用命令行参数
JVM的命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
HotSpot参数分类:
- 标准: - 开头,所有的HotSpot都支持
- 非标准:-X 开头,特定版本HotSpot支持特定命令
- 不稳定:-XX 开头,下个版本可能取消
常用参数:
-Xmn10M新生代-Xms40M最小堆-Xmx60M最大堆,最小堆和最大堆最好保持一致,因为动态扩容耗费性能-XX:+PrintCommandLineFlags打印已经被设置好的XX参数-XX:+PrintGC打印GC信息-XX:+PrintGCDetails打印详细GC信息-XX:+PrintGCTimeStamps打印GC产生时系统时间-XX:+PrintGCCause打印GC产生原因-XX:+PrintFlagsInitial打印默认参数值-XX:+PrintFlagsFinal打印最终参数值,可以通过java -XX:+PrintFlagsFinal | grep xxx来寻找想要了解的参数
GC日志
PS + PO GC日志
随便写一个会OOM的程序,并通过java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC XXX,得到以下日志
[GC (Allocation Failure) [PSYoungGen: 7294K->768K(9216K)] 7294K->4872K(39936K), 0.0025240 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 96K->0K(9216K)] [ParOldGen: 36481K->34433K(51200K)] 36577K->34433K(60416K), [Metaspace: 2674K->2674K(1056768K)], 0.0049010 secs] [Times: use r=0.00 sys=0.00, real=0.00 secs]
GC/Full GC:YGC/FGC Allocation Failure/Ergonomics:GC原因 [PSYoungGen: 7294K->768K(9216K)] 7294K->4872K(39936K), 0.0025240 secs]:PS的年轻代,回收前7294K,回收后768K,总共9216K。回收前堆占7294K,回收后堆占4872K,堆总空间39936K。 [Times: user=0.00 sys=0.00, real=0.00 secs]:相当于time ls,分别表示用户态、内核态和总共耗时
上面的命令加上-XX:+PrintGCDetails之后可以看到具体的堆信息:
Heap
PSYoungGen total 8704K, used 7483K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 91% used [0x00000000ff600000,0x00000000ffd4efa0,0x00000000ffe00000)
from space 512K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000ffe80000)
to space 1536K, 0% used [0x00000000ffe80000,0x00000000ffe80000,0x0000000100000000)
ParOldGen total 51200K, used 49775K [0x00000000fc400000, 0x00000000ff600000, 0x00000000ff600000)
object space 51200K, 97% used [0x00000000fc400000,0x00000000ff49bdc0,0x00000000ff600000)
Metaspace used 2708K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 291K, capacity 386K, committed 512K, reserved 1048576K
上述部分的左边这一列比较清晰就不解释了,而eden space 8192K, 91% used [0x00000000ff600000,0x00000000ffd4efa0,0x00000000ffe00000)括号中的三个值分别表示内存起始地址、使用空间结束地址以及整体空间结束地址。具体来说就是,从0x00000000ff600000到0x00000000ffd4efa0用了91%,到0x00000000ffe00000一共8192K。
Metaspace中,used表示以使用的,capacity表示总容量,committed表示虚拟内存占用,reserved表示虚拟内存保留,意思是保留了这么多容量但是实际上还没用这么多。
CMS GC日志
说在前面:看CMS的日志,实际上只需要看GC是否频繁,GC时常是否在允许范围内。
执行命令:java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC XXX
[GC (Allocation Failure) [ParNew: 7294K->656K(9216K), 0.0023709 secs] 7294K->4756K(39936K), 0.0026376 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
表示YGC中,年轻代的内存前后对比,与PS + PO一致
以下是CMS GC日志详细介绍:
// 初始标记
// 20103K(30720K) : 老年代使用(最大)
// 22180K(39936K) : 整个堆使用(最大)
[GC (CMS Initial Mark) [1 CMS-initial-mark: 20103K(30720K)] 22180K(39936K), 0.0003192 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// 并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// 清理之前的一个阶段
// 标记Card Table为Dirty,也称为Card Marking
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs][GC (Allocation Failure) [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-abortable-preclean-start]
// 重新标记
// Rescan (parallel):并发标记STW下的存活对象
// weak refs processing: 弱引用处理
// class unloading: 卸载用不到的class
// scrub symbol(string) table: (仅仅表示个人理解)前面卸载了一部分class,此过程是清理这些class内部引用,包括符号引用等
// CMS-remark: 47749K(51200K): 阶段过后的老年代占用及容量
// 53033K(60416K): 阶段过后的堆占用及容量
[GC (CMS Final Remark) [YG occupancy: 5283 K (9216 K)][Rescan (parallel) , 0.0008456 secs][weak refs processing, 0.0000351 secs][class unloading, 0.0002392 secs][scrub symbol table,
0.0004485 secs][scrub string table, 0.0002782 secs][1 CMS-remark: 47749K(51200K)] 53033K(60416K), 0.0024793 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// 并发清除
// 标记已经完成,进行并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
// 重置内部结构,为下次GC做准备
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
G1 GC日志
执行java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseG1GC XXX
// 巨型对象分配
[GC pause (G1 Humongous Allocation) (young), 0.0026908 secs]
// young -> 年轻代 Evacuation-> 复制存活对象
// initial-mark 混合回收的阶段,这里是YGC混合老年代回收,即Mixed GC
// [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0026908 secs]
[Parallel Time: 1.1 ms, GC Workers: 4] // 4个GC线程
[GC Worker Start (ms): Min: 249.9, Avg: 250.4, Max: 250.9, Diff: 1.0]
// 从根对象搜索
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.4]
// 更新了多少RSet
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.0, Avg: 0.4, Max: 0.8, Diff: 0.8, Sum: 1.6]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.2]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 0.0, Avg: 0.5, Max: 1.1, Diff: 1.0, Sum: 2.1]
[GC Worker End (ms): Min: 250.9, Avg: 250.9, Max: 250.9, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
// 清理Card Table
[Clear CT: 0.1 ms]
[Other: 1.5 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
// Heap: 31.3M(59.0M)->29.8M(59.0M)此处回收后占用没有减少多少,说明出现了内存泄漏
[Eden: 1024.0K(8192.0K)->0.0B(9216.0K) Survivors: 2048.0K->1024.0K Heap: 31.3M(59.0M)->29.8M(59.0M)]
[Times: user=0.03 sys=0.00, real=0.01 secs]
// 以下是混合回收其他阶段
[GC concurrent-root-region-scan-start]
[GC pause (G1 Humongous Allocation) (young)[GC concurrent-root-region-scan-end, 0.0006797 secs]
// 无法进行复制Evacuation时,进行FGC
// 使用G1需要尽量避免FGC,或者避免频繁FGC
[Full GC (Allocation Failure) 29M->29M(60M), 0.0031303 secs]
[Eden: 0.0B(9216.0K)->0.0B(10.0M) Survivors: 1024.0K->0.0B Heap: 29.8M(60.0M)->29.6M(60.0M)], [Metaspace: 2674K->2674K(1056768K)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC remark, 0.0000366 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
调优
首先了解一些基本概念:
- 吞吐量:用户代码时间 /(用户代码执行时间 + 垃圾回收时间);
- 响应时间:STW越短,响应时间越好。
条有前需要明白目标是什么,是吞吐量优先,还是响应时间优先?或者说在满足一定的响应时间的情况下,要求达到多大的吞吐量等等。
调优分三个时间点:
- 根据需求进行JVM规划和预调优;
- 出现程序运行缓慢和卡顿时,优化运行JVM运行环境;
- 解决JVM运行过程中出现的各种问题,比如OOM。
JVM规划与预调优
首先要明白,预调优需要在确定业务场景下去做,并且需要有监控或者压测,看到实际结果后才知道调优结果。
- 熟悉业务场景,根据业务场景选择回收器组合:
- 响应时间优先,使用CMS、G1、ZGC等;
- 吞吐量优先,使用PS + PO的组合。
- 计算内存需求,在合理范围内(可通过压测模拟系统运行高峰期,满足业务TPS需求,然后计算内存占用量),小内存会让YGC变得频繁快速,也是撑得住的;
- 选定CPU,性能越高越好,因为多线程运行会让并发GC的GC线程减少对工作线程的影响,使得一次回收时间短;
- 设定日志参数
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause即五个日志文件,每个20M,整体大小100M;- 或者每天产生一个日志文件。
- 最后必须要观察日志,视具体情况进行改进。
系统排查
-
系统CPU飙高,说明一定有线程在占用系统资源:
top找出哪个进程CPU高top -Hp pid该进程中的哪个线程CPU高jstack -l pid查找哪个栈帧消耗时间,判断工作线程和GC线程哪个占比高
-
系统内存飙高:
jmap查看内存信息- 使用jhat、jvisualvm、mat、jprofiler、arthas等进行文件分析
-
JVM监控:
- jstat 看起来不方便
- jconsole 图形界面不好看
- jvisualvm 推荐
- jprofiler 收费
以上是图像化界面,线上系统是不会用图图形界面的,但是可以用arthas和cmdline等,但是可用在测试。
部分命令的详细说明:
jstack -l
nid表示线程的16进制- 重点关注
WAITING BLOCKED,说明这个线程阻塞了 waiting on <0x0000000088ca3310> (a java.lang.Object)表示该线程正在等待该锁的释放- 再来搜索
0x0000000088ca3310,关注RUNNABLE的线程中哪些持有上述锁 - 最后可以在业务代码中寻找问题
jmap
jmap -histo pid查看哪个对象比较多jmap -dump:format=b,file=xxx pid
但是线上系统不建议执行jmap -dump,因为内存特别大,jmap执行期间会对进程产生很大影响,甚至卡顿。
- 可以设定参数
-XX:+HeapDumpOnOutOfMemoryError等系统挂了默认生成文件 - 系统挂了,在重启前也可以使用
jmap - 在高可用的服务器中,停下一台来用
jmap -dump,不会影响主服务
jhat
jhat -J-mx512M xxx.dump解析文件,通过http://xxx:7000,查看"Instance Counts for All Class"来查看哪些类产生了多少对象。- // TODO OQL
总结简单流程:
top查看哪个进程CPU高jstack查看工作线程问题jmap -histo查看哪个对象多,可能导致频繁GC
// TODO 举个例子
GC参数
GC常用参数
- -Xmn -Xms -Xmx -Xss 年轻代 最小堆 最大堆 栈空间
- -XX:+UseTLAB 使用TLAB,默认打开
- -XX:+PrintTLAB 打印TLAB的使用情况
- -XX:TLABSize 设置TLAB大小
- -XX:+DisableExplictGC System.gc()不管用 ,FGC
- -XX:+PrintGC 打印gc信息
- -XX:+PrintGCDetails 详细信息
- -XX:+PrintHeapAtGC gc打印堆栈情况
- -XX:+PrintGCTimeStamps 打印发生gc的系统时间
- -XX:+PrintGCApplicationConcurrentTime (低) 打印gc的时候应用程序时间
- -XX:+PrintGCApplicationStoppedTime (低) 打印暂停时长 STW
- -XX:+PrintReferenceGC (重要性低) 记录回收了多少种不同引用类型的引用
- -verbose:class 类加载详细过程
- -XX:+PrintVMOptions 打印jvm运行参数
- -XX:+PrintFlagsFinal -version | grep *** 查询jvm参数 -XX:+PrintFlagsInitial 必须会用
- -Xloggc:opt/log/gc.log 记录gc日志
- -XX:MaxTenuringThreshold cms默认6 其他15 升代年龄,最大值15
- 锁自旋次数 -XX:PreBlockSpin 热点代码检测参数(代码执行多少次后jit)-XX:CompileThreshold 逃逸分析 标量替换 ... 这些不建议设置
Parallel常用参数
- -XX:SurvivorRatio 比例 默认8:1:1
- -XX:PreTenureSizeThreshold 大对象到底多大 超过进入old区
- -XX:MaxTenuringThreshold cms默认6 其他15 升代年龄,最大值15
- -XX:+ParallelGCThreads 并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
- -XX:+UseAdaptiveSizePolicy 自动选择各区大小比例
CMS常用参数
- -XX:+UseConcMarkSweepGC
- -XX:ParallelCMSThreads CMS线程数量 默认核的一半
- -XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
- -XX:+UseCMSCompactAtFullCollection 在FGC时进行压缩 原本是mark sweep标记清理
- -XX:CMSFullGCsBeforeCompaction 多少次FGC之后进行压缩
- -XX:+CMSClassUnloadingEnabled 回收metaspace或永久待不用的klass
- -XX:CMSInitiatingPermOccupancyFraction 1.7之前 达到什么比例时进行Perm回收
- GCTimeRatio 设置GC时间占用程序运行时间的百分比
- -XX:MaxGCPauseMillis 停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代
G1常用参数
- -XX:+UseG1GC
- -XX:MaxGCPauseMillis 建议值,G1会尝试调整Young区的块数来达到这个值
- GCTimeRatio GC时间建议比例,G1会根据这个值调整堆空间
- -XX:GCPauseIntervalMillis ?GC的间隔时间
- -XX:+G1HeapRegionSize 分区大小,建议逐渐增大该值,1 2 4 8 16 32。 随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长 ZGC做了改进(动态区块大小)
- G1NewSizePercent 新生代最小比例,默认为5%
- G1MaxNewSizePercent 新生代最大比例,默认为60%
- ConcGCThreads 线程数量
- InitiatingHeapOccupancyPercent 启动G1的堆空间占用比例
几个GC的具体调优方法
- PS + PO以及PN + CMS如何让系统基本不产生FGC
- 加大内存
- 加大Young的比例
- 提高YGC的年龄
- 提高Survivor区比例
- 避免代码内存泄漏
- 如何避免G1产生FGC
- 加大内存
- 提高CPU性能,因为GC速度快,业务逻辑产生对象的速度固定,最终使得内存空间变大
- 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)
- 如何避免CMS请出Serial Old
-XX:CMSInitiatingOccupancyFraction表示CMS FGC的阈值。可以降低该阈值,让老年代保持足够空间;-XX:+UseCMSCompactAtFullCollection表示FGC时使用Mark Compact,减少内存碎片化,但是会增加并发清理的时间;-XX:CMSFullGCsBeforeCompaction表示多少次FGC后进行压缩,也可以一定程度减少内存碎片化。