大家好,我是小水珠。
JVM调优是一个系统而又复杂的过程,但我们知道,在大多数情况下,我们基本不用去调整JVM内存分配,因为一些初始化的参数已经可以保证应用服务正常稳定的工作了。
但是所有的调优都是有目标性的,JVM内存分配调优也一样。没有性能问题的时候,我们自然不会随意改变JVM内存分配的参数。那有了问题呢?有了什么样的性能问题我们需要对其进行调优呢?又该如何调优呢?这就是我今天要分享的内容。
一 JVM内存分配性能问题
其实在很多时候,在应用服务的特定场景下,JVM内存分配不合理带来的性能表现并不会像内存溢出问题这么突出。可以说如果你没有深入到各项性能指标中去,是很难发现其中隐藏的性能损耗。
JVM内存分配不合理最直接的表现就是频繁的GC,这会导致上线文切换等性能问题,从而降低系统的吞吐量,增加系统的响应时间。因此,如果你在线上环境或性能测试时,发现频繁的GC,且是正常的对象创建和回收,这个时候就需要考虑调整JVM内存分配了,从而减少GC锁带来的性能开销。
二 对象在堆中的生命周期
当我们新建一个对象时,对象会被优先分配到新生代的Eden区中,这时虚拟机会给对象定义一个对象年龄计数器(通过参数-XX:MaxTenuringThreshold设置)。
同时,也有另外一种情况,当Eden空间不足时,虚拟机将会执行一个新生代的垃圾回收(Minor GC)。这时JVM会把存活的对象转移到Survivor中,并给对象的年龄+1。对象在Survivor中同样也会经历MinorGC,每经过一次MinorGC,对象的年龄将会+1。
三 查看JVM堆内存分配
我们可以通过以下命令来查看堆内存配置的默认值:
- java -XX:+PrintFlagsFinal -version | grep Heapsize
- java -heap 17284
通过命令,我们可以获得在这台机器上启动的JVM默认最大堆内存为1953MB,初始化大小为124MB。
四 JVM内存分配的调优过程
我们先使用JVM的默认配置,观察应用服务的运行情况,下面我将结合一个实际案例来讲述。现模拟一个抢购接口,假设需要满足一个5W的并发请求,且每次请求会产生20KB对象,我们可以通过千级并发创建一个1MB对象的接口来模拟万级并发请求产生大量对象的场景,具体代码如下:
1. AB压测
分别对应用服务进行压力测试,以下是请求接口的吞吐量和响应时间在不同并发用户数下的变化情况:
可以看到,当并发数量到了一定值时,吞吐量就上不去了,响应时间也迅速增加。那么,在JVM内部运行又是怎样的呢?
2. 分析GC日志
此时我们可以通过GC日志查看具体的回收日志。我们通过设置JVM配置参数,将运行期间的GC日志dump下来,具体配置参数如下:
- -XX:+PrintGCTimeStamps
- -XX:+PrintGCDetails
- -Xloggc:/log/heapTest.log
GCViewer主页面显示FullGC发生了13次,右下角显示年轻代和老年代的内存使用率几乎达到了100%。而FullGC会导致stop-the-world的发生,从而严重影响到应用服务的性能。此时,我们需要调整堆内存的大小来减少FullGC的发生。
3. 参考指标
- GC频率
高频的FullGC会给系统带来非常大的性能消耗,虽然MinorGC相对于FullGC来说好了许多,但过多的MinorGC仍会给系统带来压力。
- 内存
这里的内存指的是堆内存大小,堆内存又分为年轻代内存和老年代内存。首先我们要分析堆内存大小是否合适,其实是分析年轻代和老年代的比例是否合适。如果内存不足或分配不均匀,会增加FullGC,严重的将会导致CPU持续爆满,影响系统性能。
- 吞吐量
频繁的FullGC将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降。
- 延时
JVM的GC持续时间也会影响到每次请求的响应时间。
4. 具体调优方法
- 调整堆内存空间减少FullGC
java -jar -Xms4g -Xmx4g heapTest-0.0.1-SNAPSHOT.jar
以下是各个配置项的说明:
- -Xms:堆初始大小
- -Xmx:堆最大值
调大堆内存之后,我们再来测试下性能情况,发现吞吐量提高了40%左右,响应时间也降低了将近50%。
再查看GC日志,发现FullGC频率降低了,老年代的使用率只有16%了。
- 调整年轻代减少MinorGC
java -jar -Xms4g -Xmx4g -Xmn3g heapTest-0.0.1-SNAPSHOT.jar
再进行AB压测,发现吞吐量上去了。
再看GC日志,发现MinorGC也明显降低了,GC花费的总时间也减少了。
- 设置Eden,Survivor区比例
在JVM中,如果开启AdaptiveSizePolicy,则每次GC后都会重新计算Eden,From Survivor和To Survivor区的大小,计算依据是GC过程中统计的GC时间,吞吐量,内存占用量。这个时候SurvivorRatio默认设置的比例会失效。
在JDK1.8中,默认是开启AdaptiveSizePolicy的,我们可以通过-XX:UseAdaptSizePolicy关闭该配置项,或显示运行-XX:SurvivorRatio=8将Eden,Survovor的比例设置为8:2。
再进行AB性能测试,我们可以看到吞吐量提升了,响应时间降低了。
五 总结
JVM内存调优通常和GC调优是互补的,基于以上调优,我们可以继续对年轻代和堆内存的垃圾回收算法进行调优。这里可以结合上一讲的内容,一起完成JVM调优。
虽然分享了一些JVM内存分配调优的常用方法,但我还是建议你在进行性能压测后如果没有发现突出的性能瓶颈,就继续使用JVM默认参数,起码在大部分场景下,默认配置已经可以满足我们的需求了。但满足不了也不要慌张,结合今天所学的内容区实践一下,相信你会有新的收获。