Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
Full GC:发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
1. 内存分配策略
1.1 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
测试程序:
尝试分配3个2MB
大小和1个4MB
大小的对象,-Xms20M -Xmx20M -Xmn10M
这3个参数限制了Java堆大小为20MB
,不可拓展,其中10MB
分给新生代,剩下10MB
分配给老年代。
-XX:SurvivorRatio=8
规定Eden:Survivor
为8:1
,所以新生代中可用空间为9MB
。
public class Main {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] all1, all2, all3, all4;
all1 = new byte[2 * _1MB];
all2 = new byte[2 * _1MB];
all3 = new byte[2 * _1MB];
all4 = new byte[2 * _1MB]; // 出现一次 Minor GC
}
}
运行参数:
java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 Main
运行结果:
[GC (Allocation Failure) [PSYoungGen: 6651K->368K(9216K)] 6651K->6520K(19456K), 0.0041477 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 368K->0K(9216K)] [ParOldGen: 6152K->6410K(10240K)] 6520K->6410K(19456K), [Metaspace: 2507K->2507K(1056768K)], 0.0035600 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 9216K, used 2212K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff8290e0,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 6410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 62% used [0x00000000fec00000,0x00000000ff242978,0x00000000ff600000)
Metaspace used 2514K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 264K, capacity 386K, committed 512K, reserved 1048576K
其中PSYoungGen
是新生代,ParOldGen
是老年代,Metaspace
class space
属于永久代。
分配all1,all2,all3
时新生代空间还够,当分配 all4
时新生代空间不足,发生Minor GC
,GC期间虚拟机发现已有的3个2MB
对象无法全部放入Survivor
空间,所以通过分配担保机制转移到老年代。
GC
之后all4
分配到Eden
空间(占用2MB
),老年代占用6MB
,通过日志可以看到这个过程。
1.2 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold
,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间发生大量的内存复制。
1.3 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden
出生并经过 Minor GC
后依然存活的将移动到 Survivor
中,每经过这样的操作年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold
用来定义年龄的阈值。
1.4 动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold
才能晋升老年代,如果在 Survivor
中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold
中要求的年龄。
1.5. 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure
设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC(如果失败则取消Minor GC,进行Full GC,再进行Minor GC);如果小于,或者 HandlePromotionFailure
设置不允许冒险,那么就要进行一次 Full GC。
2. Full GC 的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
-
调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 -
老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过-XX:MaxTenuringThreshold
调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 -
空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。 -
Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报Concurrent Mode Failure
错误,并触发 Full GC。 -
JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出java.lang.OutOfMemoryError
。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。