优先在 Eden 区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
注意即使程序什么也不做,新生代也会使用 2000k 左右的内存
HotSpot 虚拟机提供了-XX:+PrintGCDetails
这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。下面我们来进行实际测试以下。
public class Main { private static final int _1MB = 1024 * 1024; /** * 虚拟机参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC */ public static void testAllocation() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC } public static void main(String[] args) { testAllocation(); } }
在上面代码的 testAllocation() 方法中,尝试分配三个 2MB 大小和一个 4MB 大小的对象,在运行时通过-Xms20M
、-Xmx20M
、-Xmn10M
这三个参数限制了 Java 堆大小为 20MB,不可扩展,其中 10MB 分配给新生代,剩下的 10MB 分配给老年代。-XX:Survivor-Ratio=8
决定了新生代中 Eden 区与一个 Survivor 区的空间比例是8∶1,从输出的结果也清晰地看到 “eden space 8192K、from space 1024K、to space 1024K” 的信息,新生代总可用空间为 9216KB(Eden 区 + 1 个 Survivor 区的总容量)。
执行 testAllocation() 中分配 allocation4 对象的语句时会发生一次 Minor GC,这次回收的结果是新生代 8137KB 变为 611KB,而总内存占用量则几乎没有减少(因为allocation1、2、3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。产生这次垃圾收集的原因是为 allocation4 分配内存时,发现 Eden 已经被占用了 6MB,剩余空间已不足以分配 allocation4 所需的 4MB 内存,因此发生 Minor GC。垃圾收集期间虚拟机又发现已有的三个 2MB 大小的对象全部无法放入 Survivor 空间(Survivor空间只有 1MB 大小),所以只好通过分配担保机制提前转移到老年代去。这次收集结束后,4MB 的 allocation4 对象顺利分配在 Eden 中。因此程序执行完的结果是 Eden 占用 4873KB 约等于 4MB(被 allocation4 占用),Survivor 空闲,老年代被占用 6144KB 约等于 6MB(被 allocation1、2、3 占用)。通过 GC 日志可以证实这一点。
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
大对象直接进入老年代主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
设置对象直接进入老年代大小限制,单位是字节,只对 Serial 和 ParNew 两款收集器有效。
-XX:PretenureSizeThreshold
public class Main { private static final int _1MB = 1024 * 1024; /** * 虚拟机参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728 */ public static void testPretenureSizeThreshold() { byte[] allocation; allocation = new byte[4 * _1MB]; //直接分配在老年代中 } public static void main(String[] args) { testPretenureSizeThreshold(); } }
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
动态对象年龄判定
为了能更好地适应不同程序的内存状况,HotSpot 虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold
才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold
中要求的年龄。
主要进行 GC 的区域
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
空间分配担保
我们知道,新生代采用的是复制算法清理内存,每一次 Minor GC,虚拟机会将 Eden 区和其中一块 Survivor 区的存活对象复制到另一块 Survivor 区,但当出现大量对象在一次 Minor GC 后仍然存活的情况时,Survivor 区可能容纳不下这么多对象,此时,就需要老年代进行分配担保,即将 Survivor 无法容纳的对象直接进入老年代。
这么做有一个前提,就是老年代得装得下这么多对象。可是在一次 GC 操作前,虚拟机并不知道到底会有多少对象存活,所以空间分配担保有这样一个判断流程:
- 发生 Minor GC 前,虚拟机先检查老年代的最大可用连续空间是否大于新生代所有对象的总空间;
- 如果大于,Minor GC 一定是安全的;
- 如果小于,虚拟机会查看 HandlePromotionFailure 参数,看看是否允许担保失败;
- 允许失败:尝试着进行一次 Minor GC;
- 不允许失败:进行一次 Full GC;
不过 JDK 6 Update 24 后,HandlePromotionFailure 参数就没有用了,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC,这种其实有点赌的动机,如果这次垃圾收集情况特殊,那么尝试 Minor GC 就会出现担保失败,担保失败还是会进行 Full GC,这样停顿时间就更长了。