JVM堆内存分配的调优过程
在JVM内存分配中,我们提出了一个问题,当遇到JVM内存性能问题时,应该如何调优?接下来我们通过一个案例来回答这个问题。
一、案例场景
现模拟一个抢购接口,假设需要满足5W的并发请求,且每次请求会产生20KB对象。这里,我们可以通过千级并发创建一个1MB对象的接口来模拟万级并发请求产生大量对象的场景,具体代码如下:
1 |
|
注:这里我们先使用JVM的默认配置
二、AB压测
分别对应用服务进行压力测试,以下是请求接口的吞吐量和响应时间在不同并发用户数下的变化情况:
可以看到,当并发数量到了一定值时,吞吐量就上不去了,响应时间也迅速增加。那么,在 JVM 内部运行又是怎样的呢?
三、分析GC日志
此时我们可以通过GC日志查看具体的回收日志。我们可以通过设置JVM参数,将运行期间的GC日志dump下来,具体配置参数如下:
1 |
|
以下是各个配置项的说明:
- -XX:PrintGCTimeStamps,打印GC具体时间;
- -XX:PrintGCDetails,打印出GC详细日志;
- -Xloggc:path,GC日志的输出路径。
收集到GC日志后,我们就可以通过GCViewer工具打开它,进而查看到具体的GC日志如下:
主页面显示FullGC发生了13次,右下角显示新生代和老年代的内存使用率几乎达到了100%。而FullGC会导致stop-the-world的发生,从而严重影响到应用服务的性能。此时,我们需要调整堆内存的大小来减少FullGC的发生。
四、参考指标
我们可以将某些指标的预期值作为参考指标,常用的参考指标如下:
- GC频率,高频的FullGC会给系统带来非常大的性能消耗,虽然MinorGC相对FullGC来说会好许多,但过多的MinorGC仍会给系统带来压力。
- 内存,这里的内存指的是堆内存大小。堆内存又分为新生代和老年代内存。首先,我们要分析堆内存大小是否合适,其次是分析新生代和老年代的比例是否合适。如果内存不足或分配不均匀,会增加FullGC,严重的情况将导致CPU持续爆满,影响系统性能。
- 吞吐量,频繁的FullGC将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降。
- 延时,JVM的GC持续时间也会影响到每次请求的响应时间。
五、具体调优方法
5.1、调整堆内存空间
调整堆内存空间减少FullGC。通过日志分析,堆内存基本被用完了,而且存在量FullGC,这意味着我们的堆内存严重不足。这时,我们需要调大堆内存的空间。
1 |
|
说明:
- -Xms,堆初始大小
- -Xmx,堆最大值
5.2、调整新生代大小
除了调整堆内存大小外,我们还可以将新生代设置得大一些,从而减少一些MinorGC:
1 |
|
说明:
- -Xmn,新生代大小
5.3、设置Eden、Survivor区比例
在JVM中,如果开启AdaptiveSizePolicy,则每次GC后都会重新计算Eden、From Survivor和To Survivor区的大小,计算的依据是GC过程中统计的GC时间、内存占用量和吞吐量。这时,SurvivorRatio默认设置的比例会失效。
在JDK1.8中,默认是开启AdaptiveSizePolicy的,我们可以通过-XX:-UseAdaptiveSizePolicy关闭该项配置,或显示运行-XX:SurvivorRatio=8将Eden、Survivor的比例设置为8:2。大部分新对象都是在Eden区创建的,我们可以固定Eden区的占用比例,来调优JVM的内存分配性能。
六、小结
在这篇文章中,我们通过一个案例熟悉了JVM堆内存分配的调优过程。涉及到的核心概念是:
- GC日志分析是进行调优的前提。
- GC频率、堆内存大小、吞吐量、延时是确定调优方向的重要性能指标。
- 调整堆内存空间、调整新生代大小和设置Eden、Survivor区比例是调优的常用方法