堆—对象分配过程
概念
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
下面几点说明:
- new的对象先放伊甸园区,此区有大小限制;
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器对伊甸园区进行垃圾回(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区;
- 然后将伊甸园中的剩余对象移动到幸存者0区;
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区(0区、1区互相换);
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 啥时候能去养老区呢?可以设置次数。默认是15次;
- 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理;
- 若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。
可以设置参数(次数):-Xx:MaxTenuringThreshold= N进行设置。
图解过程
-
我们创建的对象,一般都是存放在Eden区的,当Eden区满了后,就会触发GC操作,一般被称为 YGC / Minor GC操作;
-
当我们进行一次垃圾收集后,红色的将会被回收,而绿色的还会被占用着,存放在S0(Survivor From)区。同时给每个对象设置了一个年龄计数器,一次回收后就是1;
-
同时Eden区继续存放对象,当Eden区再次存满的时候,又会触发一个MinorGC操作,此时GC将会把 Eden和Survivor From中的对象 进行一次收集,把存活的对象放到 Survivor To区,同时让年龄 + 1;
-
我们继续不断的进行对象生成和垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,即将年轻代中的对象晋升到老年代中;
幸存区区满了后?
- 特别注意,在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发 MinorGC 操作;
- 如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代。
举例:以当兵为例,正常人的晋升可能是 : 新兵 -> 班长 -> 排长 -> 连长。
但是也有可能有些人因为做了非常大的贡献,直接从 新兵 -> 排长。
对象分配的特殊情况
代码演示对象分配过程
示例程序:不断的创建大对象添加到 list 中:
public class HeapInstanceTest {
byte [] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapInstanceTest> list = new ArrayList<>();
while (true) {
list.add(new HeapInstanceTest());
Thread.sleep(10);
}
}
}
然后设置JVM参数:
-Xms600m -Xmx600m
之后打开VisualVM工具,通过执行上面代码,通过VisualGC进行动态化查看:
最终,在老年代和新生代都满了,就出现OOM。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.heu.heap.HeapInstanceTest.<init>(HeapInstanceTest.java:13)
at com.heu.heap.HeapInstanceTest.main(HeapInstanceTest.java:17)
总结
- 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to(s0,s1不固定);
- 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不在永久代和元空间进行收集;
- 新生代采用复制算法的目的:是为了减少内碎片。
Minor GC,MajorGC、Full GC
-
Minor GC:新生代的GC
-
Major GC:老年代的GC
-
Full GC:整堆收集,收集整个Java堆和方法区的垃圾收集
-
我们都知道,JVM的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW(stop the word)的问题,而 Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上。
-
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。针对 Hotspot VM 的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)。
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集。
- 老年代收集(MajorGC/o1dGC):只是老年代的圾收集。
- 目前,只有CMSGC会有单独收集老年代的行为;
- 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。 - 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC会有这种行为。
整堆收集(FullGC):收集整个java堆和方法区的垃圾收集。
Minor GC
- 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC,(每次Minor GC会清理年轻代的内存);
- 因为Java对象大多都具备 朝生夕灭 的特性,所以Minor GC非常频繁,一般回收速度也比较快;
- Minor GC会引发STW(stop the word),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
Major GC
Majoy GC指发生在老年代的GC,对象从老年代消失时,就说 “Major Gc” 或 “Full GC” 发生了。
- 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程),也就是在老年代空间不足时,会先尝试触发 Minor GC。如果之后空间还不足,则触发Major GC;
- Major GC的速度一般会比MinorGc慢10倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了。
Full GC
触发 Full GC 执行的情况有如下五种:
- 调用System.gc()时,系统建议执行Full GC,但是不必然执行;
- 老年代空间不足;
- 方法区空间不足;
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
- 由Eden区、survivor spacee(From Space)区向survivor spacel(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小(即年轻代、老年代的内存大小装不下该对象)。
说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些。
GC 举例
编写一个OOM的异常,不断的创建字符串示例:
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "mogu blog";
while(true) {
list.add(a);
a = a + a;
i++;
}
}catch (Exception e) {
e.getStackTrace();
}
}
}
设置JVM启动参数:
-Xms10m -Xmx10m -XX:+PrintGCDetails
打印出的日志:
[GC (Allocation Failure) [PSYoungGen: 2038K->500K(2560K)] 2038K->797K(9728K), 0.3532002 secs] [Times: user=0.01 sys=0.00, real=0.36 secs]
[GC (Allocation Failure) [PSYoungGen: 2108K->480K(2560K)] 2405K->1565K(9728K), 0.0014069 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 2288K->0K(2560K)] [ParOldGen: 6845K->5281K(7168K)] 9133K->5281K(9728K), [Metaspace: 3482K->3482K(1056768K)], 0.0058675 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 5281K->5281K(9728K), 0.0002857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 5281K->5263K(7168K)] 5281K->5263K(9728K), [Metaspace: 3482K->3482K(1056768K)], 0.0058564 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 2560K, used 60K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0f138,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 5263K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 73% used [0x00000000ff600000,0x00000000ffb23cf0,0x00000000ffd00000)
Metaspace used 3514K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 388K, capacity 390K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.heu.heap.GCTest.main(GCTest.java:20)
[GC (Allocation Failure) [PSYoungGen: 2038K->500K(2560K)] 2038K->797K(9728K), 0.3532002 secs]
- [PSYoungGen: 2038K->500K(2560K)]:年轻代总空间为 2560K ,当前占用 2038K ,经过垃圾回收后剩余500K;
- 2038K->797K(9728K):堆内存总空间为 9728K ,当前占用2038K ,经过垃圾回收后剩余797K。
触发OOM的时候,一定是进行了一次Full GC,因为只有在老年代空间不足时候,才会爆出OOM异常。
堆空间分代思想
为什么要把Java堆分代?不分代就不能正常工作了吗?
经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
- 新生代:有Eden、两块大小相同的survivor(又称为from/to或s0/s1)构成,to总为空。
- 老年代:存放新生代中经历多次GC仍然存活的对象。
其实不分代完全可以,分代的唯一理由就是优化GC性能。
- 如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。(性能低)
- 而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。(多回收新生代,少回收老年代,性能会提高很多)
对象内存分配策略
- 如果对象在Eden区出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。
- 对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代。
- 对象晋升老年代的年龄阀值,可以通过选项**-XX:MaxTenuringThreshold**来设置。
针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden:开发中比较长的字符串或者数组,会直接存在老年代,但是因为新创建的对象都是朝生夕死的,所以这个大对象可能也很快被回收,但是因为老年代触发Major GC的次数比 Minor GC要更少,因此可能回收起来就会比较慢。
- 大对象直接分配到老年代:尽量避免程序中出现过多的大对象。
- 长期存活的对象分配到老年代。
- 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- 空间分配担保: -XX:HandlePromotionFailure 。