JVM调优的两大目标是:
提高应用程序的性能和吞吐量: 通过优化JVM的垃圾回收机制、调整线程池大小和优化代码,可以提高应用程序的性能和吞吐量。
减少JVM的内存占用和垃圾回收的次数: JVM的内存占用和垃圾回收对系统性能有很大影响,因此通过调整JVM的内存参数,可以减少内存占用和垃圾回收的次数,从而提高系统的稳定性和可靠性。
在虚拟机初始化时,堆内存的全部空间都被保留下来。可以通过 -Xmx 选项指定堆的最大大小。如果 -Xms 参数指定的值小于 -Xmx 参数指定的值,那么不是所有保留的空间都会立即提交给虚拟机。这些未提交的空间在图中被称为“虚拟空间”。
需要注意的是,虚拟空间并不等同于已经分配的堆内存。老年代和新生代是堆的两个不同部分,它们都可以根据需要增长到虚拟空间的最大限制。
因此,建议根据应用程序的内存需求和性能指标来调整 -Xms 和 -Xmx 参数的值,以充分利用可用的系统内存并达到更好的性能表现。
一些参数是堆的一个部分与另一个部分的比例。例如,参数 -XX:NewRatio
表示老一代与年轻一代的相对大小。
在考虑垃圾收集性能时,除了可用内存之外,影响因素的第二个重要因素是年轻代的比例。
较大的年轻代可以减少 minor gc 的频率,但对于有限的堆大小,它也会导致老年代变小,从而增加 major gc 的频率。因此,在选择年轻代大小时,需要考虑应用程序中对象的生命周期分布。
为了取得最佳性能,建议进行一些基准测试来确定最佳的年轻代大小,以确保 minor gc 的频率不会对性能造成太大影响,并且 major gc 的频率也能保持在一个合理的范围内。
默认情况下,年轻代大小由选项 -XX:NewRatio
控制。
比如设置 -XX:NewRatio=3
表示年轻代和年老代的比例为 1:3。换句话说,eden区和survivor区的总大小将是总堆大小的四分之一。
选项 -XX:NewSize
和 -XX:MaxNewSize
限制了年轻代的大小。将这些设置为相同的值可以固定年轻代,就像将 -Xms
和 -Xmx
设置为相同的值可以固定总堆大小一样。这对于以比 -XX:NewRatio
允许的整数倍更细的粒度调整年轻代很有用。
调整幸存者空间的大小可以使用 -XX:SurvivorRatio 选项,但通常并不会对性能产生重大影响。
如果幸存者空间过小,则可能会导致对象直接溢出到老年代;如果幸存者空间过大,则会浪费内存空间。虚拟机会在每次垃圾回收时选择一个阈值数,以确保幸存者空间保持在一定的填充率。这个阈值数可以通过配置日志 -Xlog:gc,age 来查看。
对于观察应用程序中对象的生命周期分布,显示这个阈值和新生代对象的年龄非常有用。因此,建议进行一些基准测试来确定最佳的幸存者空间大小,以确保在减少对象溢出老年代的同时,也不会浪费过多的内存空间。
除非应用程序有相当严格的暂停时间要求,否则使用JVM默认的收集器。
-Xms
与 -Xmx
设置为同一值,无论扩展还是缩小空间都会进行Full GC。-Xmx
需要根据系统的配置来确定,要给操作系统和JVM本身留下一定的剩余空间。 推荐配置系统或容器里可用内存的 70-80% 最好。-Xmn
可以方便指定新生代空间的初始值和最大值。监测JVM性能:使用工具,如JConsole、VisualVM或Java Mission Control来监测JVM的性能,包括内存使用、垃圾收集频率和时间、线程使用等指标。
分析数据:收集JVM性能数据后,需要进行分析,识别性能瓶颈和潜在的问题,例如频繁的Full GC或长时间的暂停。
优化JVM设置:根据性能数据分析结果和应用程序的特性,可以调整JVM设置来优化性能。例如,增加内存大小、调整垃圾收集器、优化线程数等。
优化应用程序代码:除了调整JVM设置,还可以通过优化应用程序代码来提高性能。例如,减少对象的创建、缓存数据、避免使用同步等。
测试和验证:进行JVM调优后,需要进行测试和验证,确保调优的结果能够满足性能需求,并且不会引入新的问题或风险。
监控和维护:在应用程序运行期间,需要继续监控JVM性能,以便及时发现和解决问题,并进行维护,例如定期清理日志文件、备份数据等。
在某一天线上服务的grafna监控突然发出告警,显示服务器的CPU负载产生了异常上升,服务的接口耗时也开始突增,针对这种情况,立刻开始了排查。
火焰图如何解读可以参考:性能分析利器:火焰图。查看了CPU的火焰图看了一下,发现GC相关的函数占了一半的CPU调用,然后查看了JVM的监控,发现实例出现了频繁的Full GC,此时基本上可以确定是GC导致的问题。
重新部署了一遍,发现还是有问题,说明很可能和代码应该有关系,和部署的机器关系不大。接下来开始定位JVM的具体问题并进行优化。
在JVM启动的时候添加以下参数,就可以将GC日志打印出来:
-XX:+PrintGCDetails 打印GC日志细节
-XX:+PrintGCTimeStamps 打印GC日志时间
-Xloggc:gc.log 将GC日志输出到指定的磁盘文件上去,这里会把gc.log输出到项目根路径
其中一个日志内容如下:
2022-05-17T11:35:04.163+0800: [GC pause (G1 Evacuation Pause) (young) (to-space exhausted), 1.46786732 secs]
[Parallel Time: 1205.9 ms, GC Workers: 20]
[GC Worker Start (ms): Min: 5038821026.8, Avg: 5038821026.9, Max: 5038821027.0, Diff: 0.2]
[Ext Root Scanning (ms): Min: 0.7, Avg: 1.0, Max: 2.6, Diff: 1.8, Sum: 13.6]
[Update RS (ms): Min: 12.4, Avg: 13.8, Max: 14.7, Diff: 2.3, Sum: 179.6]
[Processed Buffers: Min: 71, Avg: 87.8, Max: 106, Diff: 35, Sum: 1141]
[Scan RS (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 2.8]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 1094.0, Avg: 1094.5, Max: 1094.7, Diff: 0.7, Sum: 22728.8]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.5]
[Termination Attempts: Min: 1, Avg: 1.1, Max: 2, Diff: 1, Sum: 14]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
[GC Worker Total (ms): Min: 609.6, Avg: 609.7, Max: 609.9, Diff: 0.2, Sum: 7926.6]
[GC Worker End (ms): Min: 5038821636.6, Avg: 5038821636.6, Max: 5038821636.6, Diff: 0.1]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.4 ms]
[Other: 320.7 ms]
[Evacuation Failure: 315.7 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.5 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.5 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 3.5 ms]
[Free CSet: 0.6 ms]
[Eden: 2176.0M(2360.0M)->0.0B(1920.0M) Survivors: 96.0M->0.0B Heap: 3764.9M(4096.0M)->1750.9M(4096.0M)]
[Times: user=6.67 sys=2.00, real=1.53 secs]
通过观察GC日志,发现了两个异常GC的场景,但是这两种场景都出现了to space exhausted的问题。打开谷歌查询了一下具体含义:
因此可以初步得出结论:服务的GC出现了问题,具体是因为出现了大量的大对象的分配造成的。在服务中新建了大量的大对象被分配到老年区,导致了老年区空间过度碎片化,而且老年区的垃圾回收只发生在mix gc和full gc,而当触发了young gc的时候,eden区存活下来的对象,升级到old区又没有足够空间用来存放,从而导致 to space exhausted。(补充说明:G1中,新创建的大对象,是直接分配到old区的,不会分配在eden区中)
现在以及基本能够确认是由于大对象引起的GC问题,导致JVM频繁的被触发FULL GC。接下来需要通过heap dump来确认这个大对象到底是个什么玩意。
使用命令行来下载服务的堆栈dump信息:
jmap -dump:format=b,file=[文件名].hprof [pid]
上图以类的维度做的统计(就是这个类在当前内存中创建的对象占用空间大小),我发现perf打点的这个类,在本服务当前内存中占比最高,且它在之前的图里确实显示产生了大对象,所以现在它成了重大嫌疑犯。
但是嫌疑犯,归嫌疑犯,毕竟没有确凿证据,且因为别的组的服务也引入的perf打点,但是他们的服务确没有问题,所以我没信心说它就是凶手。反而我更加怀疑是我heap dump的时机不对,在我dump堆栈的时候,大对象真凶是不是已经被回收了。(非OOM自动dump,所以自己主动dump的时候,可能出现dump的时机不好,导致找不到原因)
当前服务打印出的gc日志,已经不能够给我提供更多的信息了,于是我想到打印出更多的gc日志,来观察现象。新增启动参数“-XX:+PrintAdaptiveSizePolicy”。
发现大量的concurrent Cycles阶段的原因是:humongous allocation,且发现申请的大对象size 都是4210784byte,和4194329byte,并且每5s就会发生一次这样的大对象分配。到这里,可以肯定造成本次gc问题的,大对象凶手有两个,且都是同时期先后分配的。
per团队修改代码:从原来每一次聚合perf打点信息,都新创建一次对象 -> 每一次聚合perf打点信息复用同一个对象(就是首次使用创建一次对象,后面再用一直复用这个空间)
同时临时修改jvm参数,试图解决问题。
调整前的关键jvm参数:Xmx8G -Xms4G -XX:InitiatingHeapOccupancyPercent=50
调整后的关键jvm参数:Xmx8G -Xms8G -XX:InitiatingHeapOccupancyPercent=40 -XX:+UnlockExperimentalVMOptions -XX:G1MaxNewSizePercent=25
Xmx8G -Xms4G -> Xmx8G -Xms8G :锁死内存,不允许内存波动,减少消耗资源。
-XX:InitiatingHeapOccupancyPercent=50 -> 40 : 提前触发并发gc周期
新增-XX:+UnlockExperimentalVMOptions -XX:G1MaxNewSizePercent=25:限制新生区的大小上限不超过内存的25%,这样能尽量保证old区有足够的空间来存储每次young gc后存活的对象。
无论是gc次数,还是gc时间,都很明显得到了极大改善。