这篇文章通过一个简单的案例来向大家介绍一下几种JVM内存调优方式,希望大家可以通过实践将理论知识形成一个闭环,纸上谈兵是最没有意义的
一、测试案例
该接口 随机产生不同大小的对象,最大4M
/**
* 内存调优测试
* @return
*/
@GetMapping("/tuningTest")
public String tuningTest(){
List temp = new ArrayList();
// 随机产生不同大小的对象,最大4MB
int i = new Random().nextInt(8)+1;
Byte[] bytes = new Byte[1024 * 512 * i];
temp.add(bytes);
return "Success";
}
二、测试过程
首先最大堆内存设置800M,初始堆大小也是800M,测试不同并发线程相同请求量的吞吐量以及相应时间
1. 10并发线程/50000请求量 结果(吞吐量:531 平均响应时间:17)
2.20并发线程/50000请求量 结果(吞吐量:566平均响应时间:39)
3.40并发线程/50000请求量 结果(吞吐量:530平均响应时间:78)
4.50并发线程/50000请求量 结果(吞吐量:516平均响应时间:90)
从上述结果我们可以看到随着并发数量的上升,吞吐量很快就上不去了,而且响应时间却在不断上升,我们以第三次结果来看看GC 情况 (多次测试结果不一定一样,因为接口产生的对象大小随机)
YGC : 3481次 平均时间:73.9ms FGC :70次 平均时间:3.3ms
三、优化方案
1.增大堆内存 ,减少FULL GC 频率
(我们把堆内存设置到1.2G,以上述第三种情况测试,因为上述第三种吞吐量上不去了)
结果:(吞吐量:602 平均响应时间:79 FULL GC次数:28 )
可以看到虽然响应时间没变但吞吐量上去了,而且FULL GC 次数大大减小了
2.增大年轻代,减少YGC 频率
(堆内存800M不变,年轻代设置到600M)
结果:(吞吐量:914平均响应时间:49 YGC 次数:1828 49ms FULL GC次数:28 )
虽然FGC次数没变,但吞吐量达到了惊人的914 响应时间也降了下来,YGC次数几乎减少了一半
3.我们同时调大堆内存和年轻代
(堆内存1200M,年轻代900M)
结果:(吞吐量:958平均响应时间:41 YGC 次数:756 15ms FULL GC次数:23)
4.设置 Eden、Survivor 区比例
在 JDK1.8 中,默认是开启 AdaptiveSizePolicy 的,则每次 GC后都会重新计算 Eden、From Survivor 和 To Survivor 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。我们可以通过 -XX:-UseAdaptiveSizePolicy 关闭该项配置,或显示运行 -XX:SurvivorRatio=8 -XX:NewRatio=2 将 Eden、Survivor 的比例固定。
这个比例的设置情况太多了, 我就不再演示了
5.根据需求,设置垃圾回收器
这个我也就不再演示了 , 因为到了这一步都会基于场景优化了
6.调整各种阈值参数
不演示,这里DEMO情况过于简单,容易产生误导
总结
调优是一个需要基于场景、基于某种指标,大量测试得出来的一种结论,没有什么万金油的配置,实践大于一切,在复杂的业务场景下,你的理论知识终究是纸上谈兵,调优终究还是内存的利用,以及对项目的熟悉,还有业务场景的适配。
了解对象在JVM的存活周期、了解在JVM升代的过程、了解大对象产生的场景、业务场景、并发量、GC频率、GC平均时间等等,都能给你调优带来很大帮助
四、JVM相关知识
1.JVM内存相关配置参数
1. -XX:MaxTenuringThreshold:对象年龄计数器 升代的年龄阈值 2.-XX:PetenureSizeThreshold:直接分配到老年代的对象大小阈值 3. -XX:+UseAdaptiveSizePolicy:是否动态调整JVM各区域大小以及进入老年代的年龄(JDK8默认开启,建议不要随便关闭,除非你对JVM内存有了非常明确的规划) 4.-XX:MaxRAMPercentage : 以容器的大小为准,按比例分配内存给JVM 5. -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/ #{path}: 打印GC LOG文件到#{path}路径 6.-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/#{path} :服务第一次内存溢出的时候打印Dump文件到#{path}路径 7.-Xms:堆初始大小 8.-Xmx:堆最大值 9. -Xmn:年轻代大小 10. -XX:SurvivorRatio 、 -XX:NewRatio :设置Eden、Survivor的比例
2.GC垃圾回收器
回收算法类型
回收器类型以及设置参数
3.GC性能衡量指标
吞吐量 :这里的吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算 GC 的吞吐量:系统总运行时间 = 应用程序耗时 +GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于95%。停顿时间 :指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。垃圾回收频率 :多久发生一次指垃圾回收呢?通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。
4.年轻代对象进入老年代条件
1.正常晋升:年龄超过设置计数器的阈值
2.大对象晋升:大小超过设置的阈值
3.动态晋升:GC后,Survivor区域中的几个年龄对象加起来超过了Survivor区内存的一半,年龄大的晋升
4.GC后,存活下来的对象太多,此时有新来对象Survivor区放不下,存活的老对象直接晋升老年代
5.常用工具以及排查命令
1.MAT (Eclipse Memory Analyzer )工具 :分析Dump内存文件 2.GCView工具 :这可以打开GC log文件 便于分析 3.top命令:查看系统CPU 以及 内存 4.jstat 命令 5.jmap命令 6.jstack命令
6.常用内存异常
1. java heap space:堆内存溢出,FGC后依旧内存不够分配,会引起频繁FGC 2.PermGen space:方法区溢出 3.StackOverflowError: 栈溢出,每个请求的线程都有一个分配的栈大小,一般是死循环或者深度递归导致 4. GC overhead limit exceeded:超出GC开销限制,意思是GC占用大量时间释放了很小的空间,一种内存警告,多半要发生堆内存溢出
五、JVM系列文章