GC调优
Reference:https://time.geekbang.org/column/article/107396
GC算法
JVM提供了不同的回收算法来实现这⼀套回收机制,通常垃圾收集器的回收算法可以分为以下⼏种:
如果说收集算法是内存回收的⽅法论,那么垃圾收集器就是内存回收的具体实现,JDK1.7 update14 之后Hotspot虚拟机所有的回收器整理如下(以下为服务端垃圾收集器):
其实在JVM规范中并没有明确GC的运作⽅式,各个⼚商可以采⽤不同的⽅式实现垃圾收集器。我们可以通过JVM⼯具查询当前JVM使⽤的垃圾收集器类型,⾸先通过ps命令查询出经常ID,再通过jmap -heap ID查询出JVM的配置信息,其中就包括垃圾收集器的设置类型。
GC性能衡量指标
⼀个垃圾收集器在不同场景下表现出的性能也不⼀样,那么如何评价⼀个垃圾收集器的性能好坏呢?我们可以借助⼀些指标。
吞吐量:这⾥的吞吐量是指应⽤程序所花费的时间和系统总运⾏时间的⽐值。我们可以按照这个公式来计算GC的吞吐量:系统总运⾏时间=应⽤程序耗时+GC耗时。如果系统运⾏了100分钟,GC耗时1分钟,则系统吞吐量为99%。GC的吞吐量⼀般不能低于95%。
停顿时间:指垃圾收集器正在运⾏时,应⽤程序的暂停时间。对于串⾏回收器而言,停顿时间可能会⽐较长;而使⽤并发回收器,由于垃圾收集器和应⽤程序交替运⾏,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。
垃圾回收频率:多久发⽣⼀次指垃圾回收呢?通常垃圾回收的频率越低越好,增⼤堆内存空间可以有效降低垃圾回收发⽣的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增⼤堆内存空间,保证正常的垃圾回收频率即可。
GC调优策略
找出问题后,就可以进⾏调优了,下⾯介绍⼏种常⽤的GC调优策略。
1. 降低Minor GC频率
通常情况下,由于新⽣代空间较⼩,Eden区很快被填满,就会导致频繁Minor GC,因此我们可以通过增⼤新⽣代空间来降低Minor GC的频率。
可能你会有这样的疑问,扩容Eden区虽然可以减少Minor GC的次数,但不会增加单次Minor GC的时间吗?如果单次Minor GC的时间增加,那也很难达到我们期待的优化效果呀。
我们知道,单次Minor GC时间是由两部分组成:T1(扫描新⽣代)和T2(复制存活对象)。
假设⼀个对象在Eden区的存活时间为500ms,Minor GC的时间间隔是300ms,那么正常情况下,Minor GC的时间为 :T1+T2。
当我们增⼤新⽣代空间,Minor GC的时间间隔可能会扩⼤到600ms,此时⼀个存活500ms的对象就会在Eden区中被回收掉,此时就不存在复制存活对象了,所以再发⽣Minor GC的时间为:两次扫描新⽣代,即2T1。
可见,扩容后,Minor GC时增加了T1,但省去了T2的时间。通常在虚拟机中,复制对象的成本要远⾼于扫描成本。
如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反⽽会增加Minor GC的时间。如果堆中的短期对象很多,那么扩容新⽣代,单次Minor GC时间不会显著增加。因此,单次Minor GC时间更多取决于GC后存活对象的数量,⽽⾮Eden区的大小
2. 降低Full GC的频率
通常情况下,由于堆内存空间不足或老年代对象太多,会触发Full GC,频繁的Full GC会带来上下⽂切换,增加系统的性能开销。我们可以使用哪些⽅法来降低Full GC的频率呢?
减少创建⼤对象:在平常的业务场景中,我们习惯⼀次性从数据库中查询出⼀个大对象⽤于web端显⽰。例如,我之前碰到过⼀个⼀次性查询出60个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在⽼年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过Minor GC之后也会进⼊到⽼年代。这种大对象很容易产⽣较多的Full GC。
我们可以将这种大对象拆解出来,首次只查询⼀些⽐较重要的字段,如果还需要其它字段辅助查看,再通过第⼆次查询显示剩余的字段。
增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低Full GC的频率。
选择合适的GC回收器
假设我们有这样⼀个需求,要求每次操作的响应时间必须在500ms以内。这个时候我们⼀般会选择响应速度较快的GC回收器,CMS(Concurrent Mark Sweep)回收器和G1回收器都是不错的选择。
而当我们的需求对系统吞吐量有要求时,就可以选择Parallel Scavenge回收器来提⾼系统的吞吐量。
总结
垃圾收集器的种类很多,我们可以将其分成两种类型
- 响应速度快
- 吞吐量⾼
通常情况下,CMS和G1回收器的响应速度快,Parallel Scavenge回收器的吞吐量⾼。
在JDK1.8环境下,默认使⽤的是Parallel Scavenge(年轻代)+ Serial Old(⽼年代)垃圾收集器。
通常情况,JVM是默认垃圾回收优化的,在没有性能衡量标准的前提下,尽量避免修改GC的⼀些性能配置参数。如果⼀定要改,那就必须基于⼤量的测试结果或线上的具体性能来进⾏调整。