在实际的开发过程中,使用Java语言开发的应用基本上都会遇到性能问题,比如接口超时、服务器负载高、并发数低、数据库性能低或死锁等,并且现在随着互联网的发展,“猛快糙”的开发方式会让代码变得越来越臃肿,随着系统访问量的增加,各种性能问题就随之而来了。
应用的性能问题非常多,比如磁盘、内存、网络IO、应用代码、数据库、缓存、JVM等,有前辈总结过可以将Java性能优化分为4个层级:
- 应用层优化:也就是代码层,主要是代码上的优化,这个主要就要靠代码review和扎实的个人基础知识了,可以通过Java线程栈定位问题代码
- 数据库层优化:优化数据库读写方面的优化,分析SQL、定位死锁、分库分表
- 框架层优化:为应用选择合适的框架是最重要的,合适的框架能够带来更优的性能
- JVM层优化:JVM是应用的最底层,属于是最难也是最容易出现性能瓶颈的一层,GC、JVM参数合理使用
优化难度逐层增加,涉及的知识和解决的问题也不同,我们本文主要讲解一下JVM的年轻代GC方面的优化知识。
运行代码:
1 | import java.util.concurrent.locks.LockSupport; |
JVM参数设置为-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xmx32m
,运行代码,查看GC情况:
这里先说一下什么叫分配速率(Allocation Rate),分配速率是指单位时间内分配的内存量,通常使用
MB/sec
作为单位,也可以使用PB/year
来表示,分配速率过高就会严重影响程序的性能,在JVM中会导致巨大的GC开销计算上一次GC之后与下一次GC之前的年轻代使用量,两者差值除以时间,就是分配速率
从上图GC日志中,我们计算一下信息:
- 在JVM启动后391ms,共创建8704kb的对象,第一次YGC之后,年轻代中还有1004kb的存活对象
- 在JVM启动后459ms,年轻代的使用量再次增加到9708kb,触发第二次YGC,GC之后年轻代的使用量缩减到1004kb
- 在JVM启动后471ms,年轻代的使用量为9708kb,GC后为1020kb
然后我们现在来计算一下这三次GC的分配速率:
Event | Time | YGC before | YGC after | Allocated During | Allocation Rate |
---|---|---|---|---|---|
1st YGC | 391ms | 8704kb | 1004kb | 8704kb | 22MB/sec |
2nd YGC | 459ms | 9708kb | 1004kb | 8704kb | 51MB/sec |
3rd YGC | 471ms | 9708kb | 1020kb | 8704kb | 709MB/sec |
Total | 471ms | 26112kb | 55MB/sec |
从表中我们看到,该程序的内存分配速率在55MB/sec。
分配速率的意义
分配速率的变化会增加或降低STW的频率,从而影响吞吐量,但仅仅只有年轻代的YGC会受分配速率的影响,老年代GC的频率和持续时间不收分配速率的直接影响,而是受到提升速率的影响,也就是Major GC是受Minor GC影响的。
我们知道年轻代中分为Eden、Survivor from和Survivor to三个区,因为分配速率直接影响Minor GC,所以我们先看下修改Eden的大小是否会减小Minor GC的频率,提升分配速率。
使用JVM参数-XX:NewSize
、-XX:MaxNewSize
和-XX:SurvivorRatio
设置Eden和Survivor区的大小,我们将Eden区分别设置为100M和500M,看一下GC日志:
-
Eden区100M
JVM参数:
-XX:NewSize=125m -XX:MaxNewSize=125m -XX:SurvivorRatio=8
Event Time YGC before YGC after Allocated During Allocation Rate 1st YGC 686ms 102400kb 1967kb 102400kb 146MB/sec 2nd YGC 820ms 104367kb 1548kb 102400kb 747MB/sec 3rd YGC 947ms 103948kb 1548kb 102400kb 788MB/sec Total 947ms 307200kb 317MB/sec 分配速率为317MB/sec
-
Eden区500M
JVM参数:
-XX:NewSize=625m -XX:MaxNewSize=625m -XX:SurvivorRatio=8
Event Time YGC before YGC after Allocated During Allocation Rate 1st YGC 1126ms 512000kb 1967kb 512000kb 445MB/sec 2nd YGC 1752ms 513967kb 1836kb 512000kb 799MB/sec 3rd YGC 2429ms 513836kb 1772kb 512000kb 739MB/sec Total 2429ms 1536000kb 618MB/sec 分配速率为618MB/sec
随着Eden区大小越来越大,分配速率也越来越大,因为减少了GC频率,就等于减少了任务线程的停顿,就可以做更多的工作,也就创建了更多的对象,所以对于同一个Java应用来说,分配速率越高,性能越高。
高分配速率对JVM的影响
如果创建了过多的朝生夕死的对象,Minor GC的频率就会增加,在并发较大的情况下,会严重的影响吞吐量,从上面的三个场景可以看出来,当年轻代越大时Minor GC的次数就会越来越少,但是分配速率并没有降低,如果每次GC后只有少量的对象存活,Minor GC的暂停时间也不会明显的增加。
但是有时候增加年轻代的大小并不能彻底的解决问题,我们通过工具jvisualvm查看堆信息
大部分堆内存都被Double对象占用了,这个对象是在readSensor()方法中创建的,最简单的代码层面的优化就是将包装类Double换成原生类型,因为原生类型不算是对象,所以也就不会在堆中分配内存,而是之间覆盖一个属性域即可,不会产生GC事件,所以GC基本上完全消除。并且JVM通过逃逸分析技术来避免过度分配。
1 | import java.util.concurrent.locks.LockSupport; |
控制台未输出任何GC日志,而jvisualvm上监控到的堆使用情况也极低,由此可见在代码中可以在适当情况下使用原生类型代替包装类。
总结
在年轻代使用上,应当适当的提高分配速率,减少Minor GC的频率,可以通过两种方式实现
- 增大新生代大小
- 使用原生类型代替包装类,减少堆内对象的创建
简单点说就是少创建对象、多分配空间,以减少GC次数,加大系统吞吐量