四、JVM内存调优
4.1理论部分
首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。
4.1.1系统级的调优主要的目的
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC一般由于以下几种情况:
1、老年代空间不足
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象
2、Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象
统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间
控制好新生代和旧生代的比例
3、System.gc()被显示调用
垃圾回收不要手动触发(生产环境一般配置 -XX:+DisableExplicitGC),尽量依靠JVM自身的机制
4.1.2 调优手段
调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,下面来看看各部分比例不良设置会导致什么后果
.1)新生代设置过小
一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC
2)新生代设置过大
一是新生代设置过大会导致老年代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加
一般说来新生代占整个堆1/3比较合适
3)Survivor设置过小
导致对象从eden直接到达旧生代,降低了在新生代的存活时间
4)Survivor设置过大
导致eden过小,增加了GC频率
另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收
4.1.3 简单的GC策略的设置方式
由内存管理和垃圾回收可知新生代和旧生代都有多种GC策略和组合搭配,选择这些策略对于我们这些开发人员是个难题,JVM提供两种较为简单的GC策略的设置方式
1)吞吐量优先
JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置
2)暂停时间优先
JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置
4.2实战部分
调优的一般步骤:①首先收集gc日志,②分析日志中的关键性能指标,③分析GC原因,调优JVM参数。
衡量GC的两个指标:①吞吐量 ②响应时间。理想情况下是高吞吐量,低响应时间,但现实往往两个参数是相悖的。
高吞吐量适合场景:科学计算,后台处理等弱交互场景。
高响应时间适合场景:对响应时间有要求的场景。
初始参数设置(三种收集器调优可以控制的参数)
PARALLEL_OPTION="-xx:+UseParallelGC -XX:+UseParalleloldGC -XX:MaxGCPauseMills=100 - XX:GCTimeRatio=99 -XX:YoungGenerationSizeIncrement=30" CMS_OPTION="-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 - XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCMSCompactAtFullCollection - XX:CMSFullGCsBeforeCompaction=5" G1_OPTION="-xx:+UseG1GC -Xms128M -Xmx128 -XX:MetaspaceSize=64M -XX:MaxGCPauseMillis=100 - XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3" JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_HOME/logs/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps - XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution"
4.2.1、Parallel GC调优
指导原则:
- 除非确定,否则不要设置最大堆内存
- 优先设置吞吐量目标
- 如果吞吐量目标达不到,调大最大内存,不能让OS使用Swap,如果仍然达不到,降低目标
- 如果吞吐量能达到,但GC时间太长,则设置停顿时间的目标
设置 Metaspace 大小 -XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M
添加吞吐量和停顿时间参数 -XX:GCTimeRatio=99 -XX:MaxGCPauseMillis=100
修改动态扩容增量 -XX:YoungGenerationSizeIncrement=30
4.2.2、G1 GC调优
操作网站:Universal JVM GC analyzer - Java Garbage collection log analysis made easy
指导原则:
- 年轻代大小:避免使用-Xmn、-XX:NewRatio等显式设置Young区大小,会覆盖暂停时间目标
- 暂停时间目标:暂停时间不要太严苛,其吞吐量目标是90%的应用程序时间和10%的垃圾回收时间,太严苛会直接影响到吞吐量
同样的,我们先不设置任何调优参数,只是设置一些初始参数,然后再来做对比,也是以Tomcat为例(之前Parallel GC相关的参数要去掉),如下:
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError - XX:HeapDumpPath=$CATALINA_HOME/logs/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps - XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintHeapAtGC - XX:+PrintTenuringDistribution"
和之前一样,重启Tomcat完成后,把GC日志下载到本地,然后上传到工具上进行分析,可以看到使用了G1后,停顿时间明显小了很多,但吞吐量变化不大,因为G1是停顿时间优先的收集器:
从触发GC的原因可以看到,Metaspace区域发生了一次GC,并且Young GC(Others)的次数也比较多:
同样的,在页面顶端,该工具也提示了我们可以调整Metaspace区域的大小:
那我们就和之前一样,调大Metaspace区域看看:
JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=64M ...略..."
再次将日志文件上传到可视化工具中进行分析,可以看到吞吐量上去了一些:
而且也没有再发生Full GC了:
但是从上图中可以看到Young GC的次数依然很多,我们可以试着将堆的大小调大一些看看。如下:
JAVA_OPTS="$JAVA_OPTS -Xms128M -Xmx128M ...略..."
再次将日志文件上传到可视化工具中进行分析,可以看到吞吐量和停顿时间却变长了一些:
但是Young GC的次数明显少了很多:
我们都知道G1是停顿时间优先的收集器,所以我们可以设置一个停顿时间目标,让G1自己自动调整去达到这个目标。如下:
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=100 ...略..."
再次将日志文件上传到可视化工具中进行分析,结果是吞吐量上去了一些,但停顿时间却变长了一些:
G1收集器的调优参数无非也就这几个,更多的是要对日志进行分析以及经验的积累,才能得出高效的调优方式。
4.3JVM常见配置汇总和参考文章
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
参考文章(要紧跟官方文档):
JVM层GC调优(下)
jvm的运行时数据区
The Java® Virtual Machine Specification
Metaspace
《JVM故障诊断指南》之4 —— Java 8:从持久代到metaspace | 并发编程网 – ifeve.com
压缩类空间
Java8 Non-Heap 中的metaspace 和compressed class space解释_chenchene128的博客-CSDN博客
CodeCache
java codeCache_mars_nier的博客-CSDN博客
Java 8 Migration: A Funny Thing Happened on the Way to Java 8
GC调优指南:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html
如何选择垃圾收集器
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html
G1最佳实践
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations
G1 GC的一些关键技术
Java Hotspot G1 GC的一些关键技术 - 知乎
CMS日志格式
https://blogs.oracle.com/poonam/understanding-cms-gc-logs
G1日志格式
https://blogs.oracle.com/poonam/understanding-g1-gc-logs
GC日志分析工具
GCViewer
ZGC:
JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)