JVM内存分配
我们知道,在大多数情况下,我们基本不用去调整JVM内存分配,因为一些初始化的参数已经可以保证应用服务正常稳定地工作了。但这并不意味着我们不需要理解JVM内存分配,尤其是在遇到一些性能问题时。
那么,当出现JVM内存性能问题时,我们该如何调优呢?在回答这个问题之前,我们先来了解一下JVM的内存分配。
一、JVM的内存性能问题
谈到JVM表现出的内存性能问题时,我们可能会想到一些线上的JVM内存溢出事故。但这方面的事故往往是应用程序创建对象导致的内存回收对象难,一般属于代码编程问题。
很多时候,在一些特定场景下,应用服务的JVM内存分配不合理带来的性能表现并不会像内存溢出问题这么突出。如果没有深入到各项性能指标中去,很难发现其中隐藏的性能损耗。
JVM内存分配不合理最直接的表现就是频繁GC,这会导致上下文切换等性能问题,增加系统的响应时间,从而降低系统的吞吐量。因此,如果在线程环境或性能测试时,发现频繁GC,且是正常的对象创建和回收,这时就需要考虑调整JVM内存分配,从而减少GC所带来的性能开销。
二、对象在对堆的生存周期
我们知道,在JVM内存模型中,堆被划分为新生代和老年代,新生代又被进一步划分为Eden区和Survivor区,而Survivor又是由From Survivor和To Survivor组成的。
当我们创建一个对象时,对象会被优先分配到新生代的Eden区中,这时虚拟机会给对象定义一个对象年龄计数器(通过参数-XX:MaxTenuringThreshold设置)。
此外,还有另外一种情况,当Eden空间不足时,虚拟机会执行一个新生代的垃圾回收(Minor GC)。这时,JVM会把存活的对象转移到Survivor中,并给对象的年龄+1。对象在Survivor中同样也会经历Minor GC,每经过一次Minor GC,对象的年龄就会+1。
当然,通过-XX:PetenureSizeThreshold参数可以设置直接将大对象分配到老年代。这时,如果分配的对象超过了设置的阈值,对象就会直接被分配到老年代,这样做的好处就是可以减少新生代的垃圾回收(Minor GC)。
三、查看JVM堆内存大小
在默认不配置JVM堆内存大小的情况下,JVM根据默认值来配置当前内存大小。我们可以通过以下命令,查看堆内存的配置值:
1 |
|
我们可以看到,在这台机器上启动的JVM默认最大堆内存为1956MB,初始化大小为124MB。
四、JVM堆内存分配
我们知道了一个对象从创建至回收的过程,接下来我们再来看看JVM堆内存是如何分配的。
在JDK1.7中,默认情况下新生代和老年代的比例是1:2,可以通过-XX:NewRatio重置该配置项。新生代中的Eden和To Survivor、From Survivor的比例是8:1:1,可以通过-XX:SurvivorRatio重置该配置项。需要注意的是,如果开启了-XX:+UseAdaptiveSizePolicy参数,JVM将会动态调整Java堆中各个区域的大小以及进入老年代的年龄。此时,-XX:NewRatio和-XX:SurvivorRatio将会失效。
在JDK1.8中,-XX:+UseAdaptiveSizePolicy参数默认是开启的,不要随便关闭该参数,除非已经对初始化堆内存/最大堆内存、新生代/老年代以及Eden区/Survivor区有非常明确的规划了。在默认情况下,JVM将会分配最小堆内存,新生代/老年代按1:2进行分配,Eden区和Survivor区则按8:2进行分配。
我们需要知道的是,默认分配策略未必是应用服务的最佳配置,因此可能会给应用服务带来严重的性能问题。
五、小结
在这篇文章中,我们主要讨论了如下概念:
- 频繁GC是JVM内存分配不合理的主要表现。这会导致上下文切换等性能问题,增加系统响应时间,从而降低系统吞吐量。
- 一个对象会经历从Eden区到Survivor区再到老年代的过程,在这个过程中会发生多次Minor GC。
- 通过设置-XX:PetenureSizeThreshold参数,可以将较大对象,直接分配到老年代内存中,这样可以减少Minor GC
- 默认情况下,JVM堆内存是按照新生代/老年代1:2、Eden/From Survivor/To Survivor8:1:1的比例分配的。