能够进行JVM调优的前提是对JVM的内存结构和GC机制有非常清楚的前提下,以下分别从JVM调优的思路到常见策略两个方面展开说明。
一、JVM调优思路
1.调优目标
- 降低系统资源使用率,主要包括CPU和内存(cpu和内存的占用率<=70%,极端情况不能超过90%)
- 提升系统吞吐量和响应速度;
- 缩短GC时间和频次(GC时间<1s,Full GC尽量不要发生)
2.调优步骤
- 分析GC日志和dump日志,判断系统性能瓶颈和可优化点;
- 确定优化目标,一般情况资源占用率、系统吞吐量、GC时间很难同时满足优化条件,优先满足主要指标;
- 确定优化方向和优化参数,常见是从JVM堆栈参数和GC参数入手;
- 调整参数后再进行测试,得出优化前后性能变化,找到最适合的参数;
3.JVM堆栈主要调整参数
- -Xms:设置堆的最小空间大小。
- -Xmx:设置堆的最大空间大小。
- -XX:NewSize:设置新生代最小空间大小。
- -XX:MaxNewSize:设置新生代最大空间大小。
- -XX:PermSize:设置永久代最小空间大小。
- -XX:MaxPermSize:设置永久代最大空间大小。
- -Xss:设置每个线程的堆栈大小。
4.主要查看性能工具
- Jps:java线程查看工具
常用命令有:jps -l -m -v,能够输出main类或jar的全限定名,输出传入main方法的参数,输出传入jvm的参数;
- Jstack:java线程栈的查看工具
常用命令有:jstack [pid]|grep [tid,同个这种方式可以查询特定线程tid的栈信息;
- Jmap:java中堆的查看工具
可以通过jmap -dump:format=b,file=[xxx.hprof] [pid]的命令去dump堆类信息
- jstat:java内置的资源和性能监控工具
可以通过:jstat -gc -h 20 [pid] [interval] 来查询gc信息,其中interval是打印间隔
- Jinfo:实时查看和调整jvm参数
二、常用的调优策略
1.选择合适的GC垃圾回收器
- CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
- CPU多核,关注吞吐量 ,那么选择PS+PO组合。
- CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择CMS。
- CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
参数配置:
//设置Serial垃圾收集器(新生代) 开启:-XX:+UseSerialGC //设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器 开启 -XX:+UseParallelOldGC //CMS垃圾收集器(老年代) 开启 -XX:+UseConcMarkSweepGC //设置G1垃圾收集器 开启 -XX:+UseG1GC
2.调整JVM堆栈大小
现象:垃圾收集频率非常频繁。
原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。
注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。
//设置堆初始值 指令1:-Xms2g 指令2:-XX:InitialHeapSize=2048m //设置堆区最大值 指令1: -Xmx2g 指令2: -XX:MaxHeapSize=2048m //新生代内存配置 指令1:-Xmn512m 指令2:-XX:MaxNewSize=512m
3.调整内存区域大小比率
现象:某一个区域的GC频繁,其他都正常。
原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。
注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。
参数配置:
//survivor区和Eden区大小比率 指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6 //新生代和老年代的占比 -XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
4.设置符合预期的停顿时间
现象:程序间接性的卡顿
原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。
注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾.
参数配置:
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间 -XX:MaxGCPauseMillis
5.调整对象升老年代的年龄
现象:老年代频繁GC,每次回收的对象很多。
原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。
注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。
配置参数:
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7 -XX:InitialTenuringThreshol=7
6.调整大对象的标准
现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。
原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。
注意:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。
配置参数:
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。 -XX:PretenureSizeThreshold=1000000
7.调整 JVM本地内存大小
现象:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM
原因: JVM除了堆内存之外还有一块堆外内存,这片内存也叫本地内存,可是这块内存区域不足了并不会主动触发GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,而一旦本地内存分配不足就会直接报OOM异常。
注意: 本地内存异常的时候除了上面的现象之外,异常信息可能是OutOfMemoryError:Direct buffer memory。 解决方式除了调整本地内存大小之外,也可以在出现此异常时进行捕获,手动触发GC(System.gc())。
配置参数:
XX:MaxDirectMemorySize
参考资料
- 调试排错 - Java 问题排查之Linux命令:https://www.pdai.tech/md/java/jvm/java-jvm-debug-tools-linux.html#文本操作
- 【JVM进阶之路】十:JVM调优总结:https://zhuanlan.zhihu.com/p/363961261