堆内存:划分、识别垃圾对象、分配策略和回收时机
【参考链接】
Java 垃圾收集机制http://wiki.jikexueyuan.com/project/java-vm/garbage-collection-mechanism.html
划分
堆内存划分为新生代和老年代,新生代又分为eden和survivor(伊甸园和幸存者),survivor又分为大小相同的两块from和to。
识别垃圾对象
如何识别是垃圾对象?判断可触及性
即从根节点开始是否可以访问到这个对象,如果不可以说明已经不在使用了,需要被回收。这些根节点叫做GC Roots,在Java中的GC Roots包括如下几种:
1、 方法区中的静态成员变量引用的对象
2、 栈中的栈帧中的局部变量表中引用的对象
3、 本地方法栈中的
分配策略和回收时机
结论
1、 优先在eden上分配对象,不超出eden大小的对象,直接在eden上分配
2、 优先在eden上分配对象,但是eden的剩余空间不够时,触发【MinorGC】清理空间后再分配。【MinorGC】会
1) 标记eden+from中的存活对象,将其放入到to中,清除eden+from
2) 如果from/to放不下存活对象,则将其放入到老年代中
3) from/to中存活的对象,每经历一次GC,它的年龄就会加1,当对象的年龄到达一定的大小,则将其移动到老年代中
3、 优先在eden上分配对象,当对象大小超出eden大小时,直接分配到老年代上。
4、 尝试去进行MinorGC,导致有存活对象进入老年代,当老年代的剩余空间也不够时,触发【FullGC】清理空间后再分配。【FullGC】会
标记新生代和老年代中的存活对象,清除垃圾,将存活对象整理到老年代中,清空新生代。(也会清理方法区)
MinorGC和FullGC
上面说了,GC分为两种,MinorGC和FullGC
1、 处理的区域不同,MinorGC只处理新生代,FullGC处理新生代、老年代、还有方法区
2、 MinorGC回收的频率很高,但是每次回收的耗时都很短,而FullGC回收的频率比较低,但是会消耗更多的时间
3、 MinorGC采用复制方式
|
新生代空间分为eden和survivor,survivor又分为from、to两部分。其中from和to空间可以视为用于复制的两块大小相同、可进行角色互换的空间。
在垃圾回收时,eden空间中的存活对象会被复制到未使用的survivor空间(假设是to),正在使用的survivor空间(假设是from)中的年轻对象也会被复制到to空间中。
此时eden和from中的剩余对象就是垃圾对象,可以直接清空,to空间存放次次回收后的存活对象。
下次回收时,则复制eden和to中的存活对象到from,清空eden和to,依次类推。
FullGC采用压缩方式
|
将所有存活的对象压缩到内存的一端
4、 MinorGC的日志是
FullGC的日志是
实验
前面我们讲过了,基本类型的数组是在堆上分配的,下面我们就使用字节数组,配置不同的区域大小,通过日志来观察内存的分配和回收,分别验证上面的结论
1. 优先在eden上分配对象,不超出eden大小的对象,直接在eden上分配
Java Code
1 | //-Xms20m -Xmx20m -Xmn8m -XX:SurvivorRatio=2 -XX:-UseTLAB -XX:+UseSerialGC -XX:+PrintGCDetails |
可以看到3M都分配在了eden中 (还有Class对象+String对象等占去(92%-75%)x4096K≈696K)
2.
2.1. 优先在eden上分配对象,但是eden的剩余空间不够时,触发MinorGC,标记eden+from中的存活对象,将其放入到to中,清除eden+from,再分配
Java Code
1 | //-Xms20m -Xmx20m -Xmn8m -XX:SurvivorRatio=2 -XX:-UseTLAB -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC |
分配c之前,会先进行MinorGC,将存活的1M保存到from/to中,清空eden+from/to
然后把c分配到eden上
2.2. 优先在eden上分配对象,但是eden的剩余空间不够时,触发MinorGC,标记eden+from中的存活对象,如果from/to放不下存活对象,则将其放入到老年代中,清除eden+from
Java Code
1 | //-Xms12m -Xmx12m -Xmn6m -XX:SurvivorRatio=4 -XX:-UseTLAB -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC |
分配c之前,会先进行MinorGC,将存活的2M保存到老年代中,清空eden+from/to
然后把c分配到eden上
2.3. from/to中存活的对象,每经历一次GC,它的年龄就会加1,当对象的年龄到达一定的大小,则将其移动到老年代中
默认情况下,这个数值为15,可以使用-XX:MaxTenuringThreshold来设置这个年龄
不过它指的是最大晋升年龄,是晋升老年代的充分必要条件,即到达该年龄对象必然晋升,而未到达该年龄对象也可能晋升。
实际上对象的实际晋升年龄是由虚拟机在运行时动态判断的。
Java Code
1 | //-Xms20m -Xmx20m -Xmn12m -XX:SurvivorRatio=1 -XX:-UseTLAB -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:MaxTenuringThreshold=3 |
可以看到虽然设置了MaxTenuringThreshold=3,但是在第2次MinorGC的时候就会把a从新生代放入到老年代
3. 优先在eden上分配对象,当对象大小超出eden大小时,直接分配到老年代上
Java Code
1 | //-Xms20m -Xmx20m -Xmn8m -XX:SurvivorRatio=2 -XX:-UseTLAB -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC |
要分配b时会先触发一次MinorGC,后续会把b分配到eden上
然后分配c时直接分配在了老年代上
4. 尝试去进行MinorGC,导致有存活对象进入老年代,当老年代的剩余空间也不够时,触发FullGC,标记新生代和老年代中的存活对象,清除垃圾,将存活对象整理到老年代中,清空新生代,再分配。
Java Code
1 | //-Xms16m -Xmx16m -Xmn8m -XX:SurvivorRatio=2 -XX:-UseTLAB -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC |
分配a内存前触发MinorGC,将1M移动到from/to中,a3M后续分配到eden上
分配b内存前触发MinorGC,将a3M移动到老年代中,(from/to中的1M也被移动到了老年代中),b3M后续分配到eden上
分配c内存前触发MinorGC,将b3M移动到老年代中,a3M虽然已经解除引用,但是MinorGC不会回收(老年代回收后继续增加)
分配d内存,、尝试MinorGC,将c3M移动到老年代中,老年代剩余空间不足,触发FullGC,标记新生代和老年代中的存活对象,aa1M+b3M+c3M,将存活对象全部整理到老年代中,清空新生代
d然后分配到eden上