JVM学习笔记 - 05JVM调优

447 阅读13分钟

常见垃圾回收器组合参数设定:(JDK1.8)

  1. -XX:+UseSerialGC = Serial New (DefNew) + Serial Old 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器

  2. -XX:+UseParNewGC = ParNew + SerialOld 这个组合已经很少用(在某些版本中已经废弃)

  3. -XX:+UseConcMarkSweepGC = ParNew + CMS + Serial Old

  4. -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】

  5. -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old

  6. -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)括号中的三个值分别表示内存起始地址、使用空间结束地址以及整体空间结束地址。具体来说就是,从0x00000000ff6000000x00000000ffd4efa0用了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]

调优

首先了解一些基本概念:

  1. 吞吐量:用户代码时间 /(用户代码执行时间 + 垃圾回收时间);
  2. 响应时间:STW越短,响应时间越好。

条有前需要明白目标是什么,是吞吐量优先,还是响应时间优先?或者说在满足一定的响应时间的情况下,要求达到多大的吞吐量等等。

调优分三个时间点:

  1. 根据需求进行JVM规划和预调优;
  2. 出现程序运行缓慢和卡顿时,优化运行JVM运行环境;
  3. 解决JVM运行过程中出现的各种问题,比如OOM。

JVM规划与预调优

首先要明白,预调优需要在确定业务场景下去做,并且需要有监控或者压测,看到实际结果后才知道调优结果。

  1. 熟悉业务场景,根据业务场景选择回收器组合:
  • 响应时间优先,使用CMS、G1、ZGC等;
  • 吞吐量优先,使用PS + PO的组合。
  1. 计算内存需求,在合理范围内(可通过压测模拟系统运行高峰期,满足业务TPS需求,然后计算内存占用量),小内存会让YGC变得频繁快速,也是撑得住的;
  2. 选定CPU,性能越高越好,因为多线程运行会让并发GC的GC线程减少对工作线程的影响,使得一次回收时间短;
  3. 设定日志参数
  • -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;
  • 或者每天产生一个日志文件。
  1. 最后必须要观察日志,视具体情况进行改进。

系统排查

  1. 系统CPU飙高,说明一定有线程在占用系统资源:

    • top 找出哪个进程CPU高
    • top -Hp pid 该进程中的哪个线程CPU高
    • jstack -l pid 查找哪个栈帧消耗时间,判断工作线程和GC线程哪个占比高
  2. 系统内存飙高:

    • jmap 查看内存信息
    • 使用jhat、jvisualvm、mat、jprofiler、arthas等进行文件分析
  3. 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

总结简单流程:

  • 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的具体调优方法

  1. PS + PO以及PN + CMS如何让系统基本不产生FGC
  • 加大内存
  • 加大Young的比例
  • 提高YGC的年龄
  • 提高Survivor区比例
  • 避免代码内存泄漏
  1. 如何避免G1产生FGC
  • 加大内存
  • 提高CPU性能,因为GC速度快,业务逻辑产生对象的速度固定,最终使得内存空间变大
  • 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)
  1. 如何避免CMS请出Serial Old
  • -XX:CMSInitiatingOccupancyFraction表示CMS FGC的阈值。可以降低该阈值,让老年代保持足够空间;
  • -XX:+UseCMSCompactAtFullCollection表示FGC时使用Mark Compact,减少内存碎片化,但是会增加并发清理的时间;
  • -XX:CMSFullGCsBeforeCompaction表示多少次FGC后进行压缩,也可以一定程度减少内存碎片化。