新生代垃圾回收流程
JVM将堆内存分为新生代和老年代,它们因为各自的特点而采用不同的垃圾回收算法来进行垃圾回收。
具体的可以参考另一篇文章:JVM四种垃圾回收算法https://blog.csdn.net/sunao1106/article/details/126690119?spm=1001.2014.3001.5501
这里主要给大家讲一下新生代中是如何进行垃圾回收的。
新生代中使用四种垃圾回收算法中的复制算法来进行垃圾回收的,它将新生代又分为了Eden、survivor from以及survivor to三个区域,并且它们的内存比例为8:1:1,比如一个10MB的新生代,会给Eden区分配8MB,两个survivor各分配1MB,但其实供我们使用的内存大小只有9MB,因为其中一个survivor区(survivor to)只是用来协助我们做复制操作的,它不被用来存放对象,看完下面的例子,你会明白。
新生代垃圾回收流程:
- 在Eden区内存够的情况下,创建的对象会优先选择放到Eden区;
- 如果在存入对象时,Eden区的内存不够存的,那就会进行一次minor gc垃圾回收,也就是将Eden区中存活的对象复制到survivor from中,然后再清空Eden区;
- 之后再进来的对象还是会选择放到Eden区,如果Eden区又存放不下了,这时就会将Eden区和survivor from中存活的对象都复制到survivor to中,然后清空Eden区和survivor from,并且将survivor from和survivor to进行位置交换,目的就是为了保证survivor to中不存放对象。如果这个时候新生代还是放不下这个对象,那该对象就会被放到老年代中。
- 注意,再上述的操作中,对象每移动一次位置,都会给其年龄+1(一个标识),当一个对象的年龄达到15时,就会将该对象纳入老年代中。
也很好理解,新生代的回收频率比较高,一般就是用来存放一些生存率较低的对象,而老年代中就是存放那些经常使用、不容易被垃圾回收的对象。
案例演示
首先为了方便效果,在idea中配置了一些虚拟机参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
,大概解释一下:
- -Xms20M、 -Xmx20M:设置堆的初始内存大小为20M,最大内存大小为20M;
- -Xmn10M:设置新生代的内存大小为10M;
- XX:+UseSerialGC:使用串行垃圾回收器;
- -XX:+PrintGCDetails -verbose:gc:打印GC详细信息。
看下面一段代码:
public class GCDemo01 {
private static final int _512KB = 1024 * 512;
private static final int _7MB = 1024 * 1024 * 7;
private static final int _8MB = 1024 * 1024 * 8;
private static final int _1MB = 1024 * 1024;
private static final int _2MB = 1024 * 1024 * 2;
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
}
执行一下:
可以看到Eden区被分配了8192K,两个survivor区各自1024K,正好10M左右。老年代为10240K,合起来刚好是我们分配的20M对空间大小。
也可以看到其内存占用情况,Eden区一开始就被占用了28%,也就是说Eden区还有差不多不到6M左右的剩余内存大小。老年代被占用了0%。
下面继续执行一段代码:
向list中添加5M大小的对象,此时可以看到,Eden区已经被占用了90%了。
如果不出意外,我们再去添加1MB的对象,Eden区就放不下了,继续看看他做了什么操作。
我们可以清除了看到,它进行了一次GC操作(minor gc),也就是将Eden区冲存活的对象复制到survivor from中,再对Eden区进行清空,此时Eden占用了13%,survivor from占用了64%。
接下来,如果我们再次存入对象,Eden区又存不下了,那么就会将Eden区和survivor from中存活的对象都复制到survivor to中,然后再清空Eden区和survivor from。
这里代码不好演示,因为我们代码中所有的对象都是强引用,就如上图所示,存放在survivor from中的对象都是强引用,也就是存活对象,我画个图来说明这个过程。
比如,在上一次GC之后,内存结构像这样:
这个时候,我们又存入了一个10MB的对象,发现Eden存不下了,于是又进行一次GC,也就是将Eden区和survivor from中存活的对象都复制到survivor to中,然后再清空Eden区和survivor from。
注意,这个时候并没有结束,随后又将survivor from和survivor to进行一次交换。
也就是确保survivor to不存放对象,就如上述所说的,这个区域就是用来协助我们进行对象复制的。
之后的每一次Eden区内存不够放,都按上述的操作来进行垃圾回收,如果在垃圾回收之后,发现还是不够放,那就将整个新生代的对象都放到老年代中,再清空它。
就比如,在我们上述例子中,存入12MB大小的对象。
可以看到,此时老年代占了86%,新生代只有eden区占用了53%。
若是老年代也满了,那就进行一次full gc,也就是新生代、老年代都进行回收。
若此时,还是存放不下,那就只好抛出OutOfMemoryError
。