《JVM掘坟笔记之基于UseSerialGC的年轻代老年代垃圾回收证明》

370 阅读6分钟

背景

一般遇到关于创建对象时内存不足的时候会爆出堆内存不足的情况,为了研究怎么合理避免这种情况的发生有必要了解下关于堆空间里年轻代和老年代的垃圾回收触发机制以及存放机制。那么下面就简单介绍下了。类似沟~

在这里插入图片描述

关于垃圾回收机制

说到垃圾回收,在内存不足的时候,JVM首先肯定不是直接把堆内存的eden区域直接清空或者其他内存不足的地方清空,这种操作没有道理。如果真的这样想那还有什么安全性可言。那么内心就这样了:

在这里插入图片描述
正常的JVM会启动自我检查会触发以下两种算法:

  1. 引用计数法
  2. 可达性算法

总而言之,会先标记好那些对象已经没有强引用之类的,然后根据情况进行不同的垃圾回收新生代一般会触发的是MinorGC基于复制算法的回收算法,例如下面的情况:

在这里插入图片描述

新生代的第一次MinorGC处理流程:

  1. 新生成对象的时候发现内存不够了
  2. 分析算法: 引用计数法和可达性算法进行分析寻找进行标记动作
  3. 迁移幸存区: 把存活的对象复制到幸存区,并且寿命加一。
  4. Minor GC后幸存区From和To互换

当再次内存满了之后,第二次Minor GC

  1. 先清空未引用的对象,包括幸存区的,如果有引用存入到幸存区,原来在幸存区仍然存活的对象再加一。
  2. 当寿命超过默认值的话就放到了老年代

注意:当新生代内存肯定不够的时候,会直接晋升存入老年代

在这里插入图片描述

老年代内存不足如何处理呢?

流程:

  1. 先进行Minor GC释放内存
  2. 如果还是不行触发FullGC,STW的时间更长

注意:关于STW是处理垃圾回收的一种机制,既是当触发GC操作时,先把用户线程挂起,优先处理垃圾回收线程,当垃圾回收线程结束,开始继续执行用户线程。由于复制算法会先复制一倍的原空间,并复制出存活的引用对象,虽然可以解决掉碎片空间的问题,但本身需要的双倍空间以及复制操作时间更长。在新生代,由于对象存活周期短,复制算法弊端比较少,如果加上老年代触发STW的时间就肯定会增长。

看你反应不大呢

在这里插入图片描述

那么不服就干,生死看淡

实际操作:

  • -Xms20M 初始化堆内存
  • -Xmx20M 最大堆内存
  • -Xmn10M 设置年轻代大小
  • -XX:+UseSerialGC 启用UseSerialGC
  • -XX:+PrintGCDetails 每次GC时打印详细信息。
  • -verbose:gc 在控制台输出GC情况

idea设置虚拟机参数:

在这里插入图片描述

package test;

import java.util.ArrayList;
import java.util.List;

public class TestGC {
    private static final  int _512KB=512*1024;
    private static final  int _1MB=1024*1024;
    private static final  int _6MB=6*1024*1024;
    private static final  int _7MB=7*1024*1024;
    private static final  int _8MB=8*1024*1024;

// 设置虚拟机参数:
//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static  void main(String[] arg0)
    {
    }
}

初始运行看下:这里新生代默认只消耗了30%(再简单的java程序也要加载一些内容所以就默认消耗了这么多),老年代为空from和to 的幸存区也是没有内容。可以看出新生代(new generation)的总大小是9M,8M给了eden,然后1M给了From,1M给了TO.老年代( tenured generation )给了10M

Heap
 def new generation   total 9216K, used 2497K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  30% used [0x00000000fec00000, 0x00000000fee70428, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3448K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 382K, capacity 388K, committed 512K, reserved 1048576K

开始向年轻代中添加7MB数据:

在这里插入图片描述
可见设置的新生代大小是10MB,Eden只分了8M现在添加7MB内容,默认还是有一部分,显然肯定超过了Eden大小,会触发一次垃圾回收。运行后果然出现了第一次Minor GC的日志。可以看出这次Minor GC的名称没有Full,说明不是老年代发生的垃圾回收。DefNew也代表回收的是新生代的内容。会看到From里有了数据,按道理来说应该是先放到To里,复制到了From中。下面再放一个512KB,还是没达到8M大小。
在这里插入图片描述
再加入512KB内容,看eden内容增加到了98%,下面要做的就是盛满Eden区。那么再加512KB会发生什么?
在这里插入图片描述
我们发现当新生代内存肯定不够的时候,会调用第二次但仍然触发的是新生代的Minor GC,如果发现第二次Minor GC后仍然不能存入(因为是List,只多不减),就把最大的内容(7M)放到了老年代(可是没有等到对象寿命达到默认值直接进入到老年代的)。然后空出了新生代的Eden位置。

好了,那么新生代已经证明了自己,但是上述的情况是两次回收之后发现自己无力然后才交给老年代的,下面就演示一种新生代自己玩不了直接就给老年代且不会触发垃圾回收的情况。

去掉之前的7M,换成新生代的最大值8M

在这里插入图片描述
可见没有GC日志打印,直接把对象交给了老年代管理。所以新生代空间不够的时候,老年代够得时候,直接就给老年代了。

那么就引出新的问题,如果老年代也扛不住呢? 实际操作修改8M为16M,

在这里插入图片描述
可以看出新生代直接交给老年代后,老年代扛不住这么大的内容,触发了自救机制,先是做一下Full GC 然后又问问新生代Minor GC看有没有可能放的下,然后就放弃治疗了。报出了Heap Space的内存不足。