JVM调优之GC调优——吞吐量优先(二)

GC调优之吞吐量优先

上一节简单介绍了一些调优参数和调优场景,详见:https://blog.csdn.net/Winner941112/article/details/102665707,这一节将会模拟大量用户请求来进行一个考虑吞吐量的调优。

吞吐量优先

这里使用一个简单的程序来模拟生产上的用户请求,每100毫秒创建150线程,每个线程创建一个512kb的对象,观察GC情况;对于对象存活在1s左右的场景,远远超过平时接口的响应时间要求,场景应该为吞吐量优先。模拟代码如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 启动程序,模拟用户请求
 * 每100毫秒创建150线程,每个线程创建一个512kb的对象,最多一秒同时存在1500线程,占用内存750m(75%),查看GC情况
 *
 * 对于对象存活在1s左右的场景,远远超过平时接口的响应时间要求,场景应该为吞吐量优先
 */
@SpringBootApplication
public class JvmAdjust {
    public static void main(String[] args) {
        SpringApplication.run(JvmAdjust.class, args);
        System.out.println("开始启动服务..........");
        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(()->{
            new Thread(()->{
                for (int i = 0; i < 150; i++){
                    try {
                        byte[] tmp = new byte[1024 * 512];  // 专门创建512kb的小对象
                        Thread.sleep(new Random().nextInt(1000));   // 随机睡眠一秒以内
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }, 100, 100, TimeUnit.MILLISECONDS);
    }
}
GC分析

主要查看GC导致的stop-the-world,这将导致我们的程序延时增大。

1.1、将代码打成jar包classloader-jvm-2.0.7.RELEASE.jar上传至Linux服务器,执行java -Xmx1024m -jar classloader-jvm-2.0.7.RELEASE.jar将jar包运行起来,这里给该程序分配最大1024M的堆内存。

1.2、执行命令:jcmd | grep “classloader-jvm-2.0.7.RELEASE.jar” | awk ‘{print $1}’ 可以查找到classloader-jvm-2.0.7.RELEASE.jar 的进程号。

1.3、执行命令:jmap -heap $(jcmd | grep “classloader-jvm-2.0.7.RELEASE.jar” | awk ‘{print $1}’) 可以查询到 jmap 打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况,可以看到内存使用已经超过了70%,如图:
JVM调优之GC调优——吞吐量优先(二)_第1张图片
1.4、收集GC日志(日志离线分析,主要用于检查故障看出是不是因为GC导致的程序卡顿),使用命令:java -Xmx1024m -Xloggc:/usr/local/jvmtest/gc1.log -jar classloader-jvm-2.0.7.RELEASE.jar 将日志重定向到一个文件中,方便查看。将文件下载下来使用GCViewer中打开,如图:JVM调优之GC调优——吞吐量优先(二)_第2张图片
从图中可以大致看出,Young GC发生了1385次,用时13s,释放内存79152.3M;Full GC发生了114次,总共用时7.7s,释放内存9536.2M。另外其他参数可详见:https://github.com/chewiebug/GCViewer

1.5、也可以通过jstat 动态监控GC统计信息,间隔1000毫秒统计一次,每10行数据后输出列标题;各参数代表含义如下:

参数 参数含义
S0C 当前S0容量(kB)
S1C 当前S1容量(kB)
S0U S0利用率(kB)
S1U S1利用率(kB)
EC Eden容量(kB)
EU Eden利用率(kB)
OC 老年代容量(kB)
OU 老年代利用率(kB)
MC Metaspace容量(kB)
MU Metaspace利用率(kB)
CCSC 类指针压缩空间容量(kB)
CCSU 使用的类指针压缩空间(kB)
YGC 新生代GC活动的数量
YGCT 新生代GC时间
FGC Full GC的数量
FGCT Full GC时间
GCT GC总时间

使用命令 jstat -gc -h10 $(jcmd | grep “classloader-jvm-2.0.7.RELEASE.jar” | awk ‘{print $1}’) 1000,可以在控制台看到打印的相关信息,如图:
gc1
从图中可以看到,在10s内发生了16次YGC,耗时0.396s;发生了FullGC 2次,耗时0.107s,总gc耗时0.503s。

GC调优
1.并行收集器

垃圾收集器Parallel参数调优,这些参数是JDK默认收集器,适用于吞吐量优先的场景,可配置参数如下:

参数 参数说明
-XX:+UseParallelGC 新生代使用并行回收收集器
-XX:+UseParallelOldGC 老年代使用并行回收收集器
-XX:ParallelGCThreads 设置用于垃圾回收的线程数
-XX:+UseAdaptiveSizePolicy 打开自适应GC策略

1.1、我们可以通过 通过命令查看参数:java -XX:+PrintFlagsFinal –version | grep 参数关键字,来查看当前参数是否被使用,如:java -XX:+PrintFlagsFinal -version | grep ParallelGCThreads
thread
这里可以看到有两个线程,现在在启动的时候增大线程数,观察打印信息的变化。

1.2、调大-XX:ParallelGCThreads=4
运行启动命令:java -Xmx1024m -Xloggc:/usr/local/jvmtest/gc2.log -XX:ParallelGCThreads=4 -jar classloader-jvm-2.0.7.RELEASE.jar,实时监控结果如图:

gc2
从图中可以看到,在10s内发生了4次YGC,耗时0.399s;发生了FullGC 3次,耗时0.198s,总gc耗时0.596s。

1.3、调小 -XX:ParallelGCThreads=1
运行启动命令:java -Xmx1024m -Xloggc:/usr/local/jvmtest/gc3.log -XX:ParallelGCThreads=1 -jar classloader-jvm-2.0.7.RELEASE.jar,实时监控结果如图:
gc3
从图中可以看到,在10s内发生了10次YGC,耗时0.339s;发生了FullGC 2次,耗时0.137s,总gc耗时0.476s。

总结:从以上几次试验中,线程数分别为1,2,4的时候gc总耗时分别为0.476s,0.503s,0.596s,在cpu核数较少的情况下,盲目增加线程数不一定能够达到想要的减少gc的效果。

2.CMS标记清除收集器

一些参数及说明如下表:

参数 参数说明
-XX:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:+UseParNewGC 在新生代使用并行收集器,CMS下默认开启
-XX:CMSInitiatingOccupancyFraction 设置触发GC的阈值,默认68%,如果不幸内存预留空间不够,就会引起concurrent mode failure
-XX:+UseCMSCompactAtFullCollection Full GC后,进行一次整理,整理的过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收
-XX:+UseCMSInitiatingOccupancyOnly 表示只在到达阈值的时候,才进行CMS回收
XX:+CMSIncrementalMode 使用增量模式,比较适合单CPU

这里有几点需要说明:Parallel GC无法满足应用程序延迟要求时再考虑使用CMS垃圾收集器;新版1.9里建议用G1垃圾收集器。

2.1、执行运行命令:java -Xmx1024m -Xloggc:/usr/local/jvmtest/gc4.log -XX:+UseConcMarkSweepGC -XX:ConcGCThreads=2 -jar classloader-jvm-2.0.7.RELEASE.jar,使用命令:jstat -gc -h10 $(jcmd | grep “classloader-jvm-2.0.7.RELEASE.jar” | awk ‘{print $1}’) 1000 进行实时监控,监控如图:
gc4
从图中可以看到,在10s内发生了202次YGC,耗时1.732s;发生了FullGC 6次,耗时0.067s,总gc耗时1.79s。

2.2、增加线程数量:java -Xmx1024m -Xloggc:/usr/local/jvmtest/gc5.log -XX:+UseConcMarkSweepGC -XX:ConcGCThreads=4 -jar classloader-jvm-2.0.7.RELEASE.jar,实时监控结果如图:
gc5
从图中可以看到,在10s内发生了203次YGC,耗时2.012s;发生了FullGC 7次,耗时0.096s,总gc耗时2.108s。

2.3、减少线程数量:java -Xmx1024m -Xloggc:/usr/local/jvmtest/gc5.log -XX:+UseConcMarkSweepGC -XX:ConcGCThreads=1 -jar classloader-jvm-2.0.7.RELEASE.jar,实时监控结果如图:
gc6
从图中可以看到,在10s内发生了206次YGC,耗时1.91s;发生了FullGC 7次,耗时0.085s,总gc耗时1.994s。

总结:从试验结果可以看出,cms回收器这种高频回收的机制并不适用于吞吐量优先的场景,cms回收器对gc的回收次数相较于Parallel GC有明现的增多,虽然每次回收的时间较少,但是因为增加了次数,使得在单位相同时间内gc所用的时间反而增加;另外,增加cms的gc线程的并发数量不会带来显著的效果提升,因为如果gc线程和用户线程一起运行的话,gc线程依然会和用户线程去争抢cpu。

3.G1收集器

一些参数及说明如下表:

参数 参数说明
-XX:G1HeapRegionSize=M 设置region大小,默认heap/2000
-XX:G1MixedGCLiveThresholdPercent 老年代依靠Mixed GC,触发阈值
-XX:G1OldCSetRegionThresholdPercent 被包含在一次Mixed GC中的region比例
-XX:+ClassUnloadingWithConcurrentMark G1增加并默认开启,在并发标记阶段结束后,JVM即进行类型卸载
-XX:G1NewSizePercent 新生代的最小比例
-XX:G1MaxNewSizePercent 新生代的最大比例
-XX:G1MixedGCCountTarget Mixed GC 数量控制

说明:G1垃圾收集器兼顾了吞吐量和响应时间;超过50%的Java堆被实时数据占用;建议大堆(大小约6GB或更大);GC要求有限的应用(稳定且可预测的暂停时间低于0.5s)。

3.1、使用G1垃圾收集器
命令参数:java -Xmx1024m -Xloggc:/usr/local/jvmtest/gc6.log -XX:+UseG1GC -jar classloader-jvm-2.0.7.RELEASE.jar,实时监控如图:
gc6
从图中可以看到,在10s内发生了8次YGC,耗时0.082s;发生了FullGC 0次,耗时0s,总gc耗时0.082s。可以看到,G1垃圾收集器对于gc的优化还是比较明显的。

你可能感兴趣的:(JVM调优之GC调优——吞吐量优先(二))