3. 分代垃圾回收
为什么要做这样一个区域划分呢?
主要是因为Java中有些对象需要长时间使用,长时间使用的对象,就把它放在老年代当中;而那些用完了就可以丢弃的对象,就把它放在新生代当中,这样就可以针对对象的生命周期的不同特点进行不同的垃圾回收策略;老年的的垃圾回收很久发生一次,而新生代的垃圾回收就发生的比较频繁;新生代处理的就是那些朝生夕死的对象,而老年代处理的就是那些更有价值,长时间存活的对象;这样根据不同的区域,使用不同的算法,就可以更有效的对我们的垃圾回收进行一个管理。
打一个比方:
有一栋居民楼,就好比Java中的堆内存;居民楼每家每户都要产生垃圾,这些垃圾需要一个保洁工人来处理,如果保洁员每家每户的收集垃圾处理垃圾,即扫描所有内存,这样的效率显然是不能接受的;那现实生活中我们是怎么处理的呢?我们都会在楼下设立一个专门丢弃垃圾的垃圾场,这就好比是一个新生代,这个垃圾场里的垃圾就是那些生命周期更短的垃圾,比如每天吃完的盒饭呀、每天用完的手纸呀,等等,这些都是我们垃圾场中回收更为频繁的垃圾,而每家每户里存储的垃圾,我们可以看成是处在老年代,这个垃圾就是比如家里用旧的椅子不想扔,但是它是没用的,我就把它暂存在家里面,将来等到我的空间实在紧张,屋子东西都摆不下的时候,我再让保洁员来一次大清理,把这些无用的垃圾都清理掉,但是这个耗时就比较长了,当然执行的频率也比较低,因为垃圾场每天清理一次就够了,每家每户里的垃圾相对更有价值一些,它们只需要等到整个堆的内存不足时,再去清理就可以了。
3.1. 分代垃圾回收机制的基本流程
新的对象都存放伊甸园区,伊甸园逐渐被占满了,当我再要创建对象时发现伊甸园被占满了,这时候就要触发一次垃圾回收了(新生代的垃圾回收,叫做Minor GC),使用可达性分析,利用标记-复制算法,把存活的对象
复制到幸存区To,让幸存的对象寿命加1,当然做完一次复制之后,会将From和To区交换,这时候就可以再往伊甸园区放入一下对象;
又经过一段时间,伊甸园又满了,就触发第二次垃圾回收(Minor GC ),这时候要把伊甸园区存活的对象和幸存区存活的对象都移动到TO中去,接着将From和To区交换。那当然幸存区的对象不能一直在幸存区呆着,而是当它的寿命超过了一个预知,默认的预知是只要经历了15次垃圾回收还活着,那说明这个对象价值比较高,经常在使用,那没必要一直在幸存区留着,因为在幸存区留着,以后再进行垃圾回收,还是不能回收,那怎么办呢?
假设这个对象超过了预值了,那就把它晋升到老年代,因为老年代垃圾回收频率比较低,不会轻易的把它回收掉。
老年代晋升的多了,这时候新生代满了,老年代也放满了,这时候就会触发一次Full GC 。
总结:
- 对象首先分配到伊甸园区
- 新生代空间不足时,触发Minor GC,伊甸园区和from存活的对象使用标记-复制说法复制到to中,存活的对象年龄加1,并且交换from、to。
- Minor gc 会引发 stop the world(stw),暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行,只不过Minor gc暂停的时间很短,即stw的时间较短。
- 当对象寿命超过阀值时,会晋升到老年代,最大寿命是15(因为对象头是4bit);
- 当老年代空间不足,会先尝试出发minor gc ,如果之后空间仍不足,那么触发full gc,stw的时间更长。
- 如果还是不够,就要触发out of memory错误。
3.2. 相关VM参数
3.2.1. 堆参数
| 含义 | 参数 | | —- | —- | | 堆初始大小 | -Xms | | 堆最大大小 | -Xmx或-XX:MaxHeapSize=size | | 新生代大小 | -Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size) | | 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy | | 幸存区比例 | -XX:SurvivorRadio=ratio | | 晋升阀值 | -XX:MaxTenuringThreshold=threshold | | 晋升详情 | -XX:PrintTenuringDistribution | | GC详情 | -XX:+PrintGCDetails -verbose:gc | | Full GC前Minor GC | -XX:+ScavengeBeforeFullGC |
3.3. 研究GC的过程并读懂GC日志
3.3.1. 小对象多次GC进入老年代
3.3.1.1. 初始情况
/**
* 研究GC的过程并读懂GC日志
* -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX: +PrintGCDetails -verbose:gc
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/13 17:14
*/
public class Demo6 {
private final static int _512KB = 512*1024 ;
private final static int _1MB = 1024*1024 ;
private final static int _6MB = 1024*1024*6 ;
private final static int _7MB = 1024*1024*7 ;
private final static int _8MB = 1024*1024*8 ;
public static void main(String[] args) {
}
}
运行结果:
解读参数:
- 年轻代
- 年轻代total为9216k,因为to区为1024k,这部分要保证一直是空的;
- eden:from:to = 8:1:1
- eden默认有28%,这是因为有系统的对象。
- 老年底
-
3.3.1.2. 7MB
ArrayList<byte[]> arrayList = new ArrayList<>() ;
arrayList.add(new byte[_7MB]);
触发了一次minor gc,最后的内存占用情况是:eden被占用了92%,from被占用了57%。3.3.1.3. 7MB+512K
ArrayList<byte[]> arrayList = new ArrayList<>() ;
arrayList.add(new byte[_7MB]);
arrayList.add(new byte[_512KB]);
触发一次minor gc,最后的内存占用情况是:eden被占用98%,from被占用了57%。3.3.1.4. 7MB+512K+512K
ArrayList<byte[]> arrayList = new ArrayList<>() ;
arrayList.add(new byte[_7MB]);
arrayList.add(new byte[_512KB]);
arrayList.add(new byte[_512KB]);
触发两次minor gc :- 第一次minor gc 将一部分对象存入到了from区;
- 第二次minor gc 再次发现伊甸园区内存不够,故而将一部分对象放入老年代。
最后的内存占用情况是:eden被占用8%,from被占用50%,老年代占用74%,因为总共9m,新生代放不下,所以触发minor gc,让一部分对象存入老年代。
3.3.2. 大对象直接晋升老年代
3.3.2.1. 8MB(老年代够用的情况下)
ArrayList<byte[]> arrayList = new ArrayList<>() ;
arrayList.add(new byte[_8MB]);
发现没有出发垃圾回收,因为JVM判断不管怎样8MB的对象都放不到新生代,而此时老年代可以放下,故而将大对象直接放入了老年代。(eden有30%,那是默认Java对象的占用,开始的时候测试过。见:【3.3.1.1. 初始情况】)
3.3.2.2. 8MB+8MB(老年代不够用的情况下)
ArrayList<byte[]> arrayList = new ArrayList<>() ;
arrayList.add(new byte[_8MB]);
arrayList.add(new byte[_8MB]);
第一个8M直接进入老年代可以放得下,第二个8M尝试放入老年代的时候发现内存不够,所以先触发了一次minor gc,发现还是不够,就触发了full gc,在触发full gc发现还是放不下, 就引发了 out of memory error,由以上程序我们也发现单个对象太大,就算在整个堆内存够的情况下,也有可能导致out of memory error。
3.3.3. 一个线程内存溢出,会影响其它线程吗?
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* 研究GC的过程并读懂GC日志
* -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/13 17:14
*/
public class Demo13 {
private final static int _512KB = 512*1024 ;
private final static int _1MB = 1024*1024 ;
private final static int _6MB = 1024*1024*6 ;
private final static int _7MB = 1024*1024*7 ;
private final static int _8MB = 1024*1024*8 ;
private final static int _9MB = 1024*1024*9 ;
private final static int _10MB = 1024*1024*10 ;
public static void main(String[] args) {
try {
new Thread(){
@Override
public void run() {
ArrayList<byte[]> arrayList = new ArrayList<>() ;
arrayList.add(new byte[_8MB]);
arrayList.add(new byte[_8MB]);
}
}.start();
TimeUnit.SECONDS.sleep(10);
System.out.println("main thread");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:(虽然开辟的线程出现了内存溢出,但是主线程正常输出,说明线程内导致的内存溢出,不会影响整个进程。)
当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行!进程依然正常。