注意:本文仅针对 JDK7、HotSPOT Java 虚拟机,对于 JDK8 引入的 JVM 新特性及其他 Java 虚拟机,本文不予关注。
1、JVM初识
Java HotSpot虚拟机
每个JVM的实现在对垃圾回收的原理的实现方式上会有一些不同。在收购SUN之前Oracle有JRockit JVM,收购SUN之后有了HotSpot虚拟机。目前Oracle同时维护了这两个虚拟机,并宣称将来会将两个虚拟机合并。HotSpot虚拟机是Oracle标准版平台的核心组成部分。在本垃圾回收系列文章中我们将通过HotSpot虚拟机来了解垃圾回收的基本准则。
JVM 架构
下面的这幅图概括了一个JVM中的主要组成。在JVM架构中,堆内存和垃圾回收器这两个部分和垃圾回收相关。堆内存是运行时用来存储实例对象的数据空间,垃圾回收器运行在堆内存上。现在我们大概知道它们是怎样的一个工作模式。
简单介绍Java 堆内存
在Java的内存模型中,最重要的是要了解堆内存的概念。运行时的Java实例对象存储在堆内存空间中。当一个对象不再被引用了,它变成可被从堆内存中回收空间。在垃圾回收的过程中,这些对象将被从堆内存中清除,同时它们的空间也就被回收了。堆内存的空间主要分成了三部分,
年轻代
a, Eden区(所有实例在运行时最初都分配到eden区中)
b, S0 Survivor Space(老一些的对象被从eden区移动到S0区,其实是eden区中的对象经过一次对eden区的Young GC还存活的对象被移动到S0)
c, S1 Survivor Space(再老一些的对象被从S0区移动到S1区,其实是在Young GC过程中S0区已满,则会将eden区中还存活的对象和S0区中的存活对象移动到S1区中)
老年代(经过S0,S1中几轮迭代后还存活的对象被提升到老年代),默认是15次。
永久代(包含一些元数据像类、方法等等)
下面上一下内存分布图:
2、JVM调优
众所周知,由于 Full GC 的成本远远高于 Minor GC,因此某些情况下需要尽可能将对象分配在年轻代,这在很多情况下是一个明智的选择。虽然在大部分情况下,JVM 会尝试在 Eden 区分配对象,但是由于空间紧张等问题,很可能不得不将部分年轻对象提前向年老代压缩。因此,在 JVM 参数调优时可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。
JVM参数:
-XX:+PrintGCDetails -Xmx20M -Xms20M
下图所示代码尝试分配 7MB 内存空间,观察一下它的内存使用情况:
如上图所示,年轻代的eden区分配了6M左右,顺便补充一下后面控制台中的输出from表示S0区域、to表示S1区域。
S0区域分配了1M S1分配了1M.这个时候我们需要的内存才7M在eden无法全部装下会有部分数据到老年代中。
分配相对大一些的年轻代空间,使用 JVM 参数-XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M 运行结果如下:
可以发现通过设置一个较大的年轻代预留新对象,设置合理的 Survivor 区并且提供 Survivor 区的使用率,可以将年轻对象保存在年轻代。一般来说,Survivor 区的空间不够,或者占用量达到 50%时,就会使对象进入年老代 (不管它的年龄有多大)。不知道有没有发现我们设置了10M的新生代空间=eden + from + to 的总和,然后真正的PSYYoungGen只是9=eden+from 这其实也证明了1点Survivor空间只有一个能用满,from和to循环使用。
-XX:SurvivorRatio的使用:表示2个Survivor : eden = 2:6 即一个Survivor占新生代的1/8。使用-XX:+PrintGCDetails -Xmx1000M -Xms500M -Xmn80M -XX:SurvivorRatio=6:
我们可以尝试加上-XX:TargetSurvivorRatio=90 参数,这样可以提高 from 区的利用率,使 from 区使用到 90%时,再将对象送入年老代:
如何让大对象进入年老代
我们在大部分情况下都会选择将对象分配在年轻代。但是,对于占用内存较多的大对象而言,它的选择可能就不是这样的。因为大对象出现在年轻代很可能扰乱年轻代 GC,并破坏年轻代原有的对象结构。因为尝试在年轻代分配大对象,很可能导致空间不足,为了有足够的空间容纳大对象,JVM 不得不将年轻代中的年轻对象挪到年老代。因为大对象占用空间多,所以可能需要移动大量小的年轻对象进入年老代,这对 GC 相当不利。基于以上原因,可以将大对象直接分配到年老代,保持年轻代对象结构的完整性,这样可以提高 GC 的效率。如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于 GC 来说会是一场灾难。原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。因此,在软件开发过程中,应该尽可能避免使用短命的大对象。
可以使用参数-XX:PetenureSizeThreshold 设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。参数-XX:PetenureSizeThreshold 只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数。亲测我用的eclipse和S2S都是不识别这个参数。
如何设置对象进入年老代的年龄
堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,年老对象存放在年老代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在 Eden 区,经过一次 GC 后依然存活,则被移动到 Survivor 区中,对象年龄加 1。以后,如果对象每经过一次 GC 依然存活,则年龄再加 1。当对象年龄达到阈值时,就移入年老代,成为老年对象。这个阈值的最大值可以通过参数-XX:MaxTenuringThreshold 来设置,默认值是 15。虽然-XX:MaxTenuringThreshold 的值可能是 15 或者更大,但这不意味着新对象非要达到这个年龄才能进入年老代。事实上,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阈值年龄的最大值。即,实际晋升年老代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold 中较小的那个。
参数:
-XX:+PrintGCDetails -Xmx100M -Xms100M -Xmn20M -XX:SurvivorRatio=2
-XX:TargetSurvivorRatio=90 -XX:MaxTenuringThreshold=1
b2对象4M经过一下Minor GC就被分配到年老代。如果希望对象尽可能长时间地停留在年轻代,可以设置一个较大的阈值。
稳定的 Java 堆 VS 动荡的 Java 堆
一般来说,稳定的堆大小对垃圾回收是有利的。获得一个稳定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 一样。如果这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少 GC 的次数。因此,很多服务端应用都会将最大堆和最小堆设置为相同的数值。但是,一个不稳定的堆并非毫无用处。稳定的堆大小虽然可以减少 GC 次数,但同时也增加了每次 GC 的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使 GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这样的考虑,JVM 还提供了两个参数用于压缩和扩展堆空间。
JVM 还提供了两个参数用于压缩和扩展堆空间。
-XX:MinHeapFreeRatio 参数用来设置堆空间最小空闲比例,默认值是 40。当堆空间的空闲内存小于这个数值时,JVM 便会扩展堆空间。
-XX:MaxHeapFreeRatio 参数用来设置堆空间最大空闲比例,默认值是 70。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆。
不过切记当-Xmx 和-Xms 相等时,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 两个参数无效。
比较:
-XX:+PrintGCDetails -Xms40M -Xmx40M -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=50
-XX:+PrintGCDetails -Xms10M -Xmx40M -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=50
code:
查看堆回收范围,以及回收所花的时间,结果如下:
从结果来看:Xms Xmx相等时,回收的堆范围在40M左右,回收的次数较少但是每次回收花的时间相对比较长。不过还是建议设置Xms Xmx相等,这样可以减少Gc回收的次数以及堆内存值回收波动进而导致系统性能的波动。
与此同时还介绍几个其他的优化参数:
-Xss128k:减少线程栈的大小,这样可以使剩余的系统内存支持更多的线程;
-Xmn2g:设置年轻代区域大小为 2GB;
–XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能地减少 GC 时间。
–XX:ParallelGC-Threads:设置用于垃圾回收的线程数,通常情况下,可以设置和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的;
–XX:+UseParallelOldGC:设置年老代使用并行回收收集器。
–XX:+LargePageSizeInBytes:设置大页的大小,过大的内存分页会导致 JVM 在计算 Heap 内部分区(perm, new, old)内存占用比例时,会出现超出正常值的划分,最坏情况下某个区会多占用一个页的大小。使用非占有的垃圾回收器。为降低应用软件的垃圾回收时的停顿,首先考虑的是使用关注系统停顿的 CMS 回收器,其次,为了减少 Full GC 次数,应尽可能将对象预留在年轻代,因为年轻代 Minor GC 的成本远远小于年老代的 Full GC。
–XX:ParallelGCThreads=20:设置 20 个线程进行垃圾回收;
–XX:+UseParNewGC:年轻代使用并行回收器;
–XX:+UseConcMarkSweepGC:年老代使用 CMS 收集器降低停顿;
这块内容还是挺有意思的,大家可以一起学习,有兴趣可以给我点评。