垃圾回收之垃圾回收算法(标记清除、标记整理、复制)、分代垃圾回收

·  阅读 705
垃圾回收之垃圾回收算法(标记清除、标记整理、复制)、分代垃圾回收

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

2. 垃圾回收算法

之前我们学习了如何判断一个对象是不是可以作为垃圾被回收,但是具体回收还需要依赖一些回收方面的算法,常见的有三种:
分别是标记清除、标记整理、和复制这三种算法,下面先从第一种标记清除算法来学习。
复制代码

2.1 标记清除

定义:Mark Sweep

  • 速度快
  • 会造成内存碎片

分为两个阶段:标记+清除

怎么判断一个对象是否是垃圾呢,就是沿着GC Root对象的引用链去找,扫描整个堆对象的过程中,如果发现对象确实被引用了,
那么它需要保留,如果没有GC Root直接或间接地引用它,那么它就可以当成是垃圾进行一个回收。
复制代码

标记清除算法分为两个阶段:

第一个阶段先标记,看看哪些对象可以是垃圾,第二个阶段是清除,所谓的清除就是把垃圾对象所占用的空间释放

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里可能会有一个小误区,释放是不是意味着把整个内存每个字节进行一个清零操作呢,注意:不会,它只需要把对象
所占用内存的起始、结束的地址给记录下来,放在一个空闲的地址列表里就可以了,下次再分配新对象的时候就到空闲
地址列表中去找看有没有一块足够的空间容纳新对象,如果有,那就进行一个内存分配,并不会把可以作为垃圾的对象
占用的内存做一个清零的处理。
复制代码

标记清除算法有什么优点和缺点呢?

  • 优点

    • 速度快

      清除操作只需要把垃圾对象内存的起始、结束地址作为一个记录就完成了,不需要做更多的额外处理,所以整个垃圾回收的
      速度相对是比较快的。
      复制代码
  • 缺点

    • 容易产生内存碎片

      清除以后不会再对空闲的内存空间做进一步的整理工作了,所以如果分配了一个较大的对象,比如说是数组,
      数组在分配时需要一段连续的空间,塞到(分配)每一个空白位置(空闲的内存)都塞不下,但是总的空闲
      空间是足够分配数组内存的,但是由于空间不连续(内存碎片),造成了新对象仍然不能分配一个有效的空白
      内存,还是会造成内存的溢出问题。
      复制代码

      在这里插入图片描述

2.2 标记整理

定义:Mark Compact

  • 速度慢
  • 没有内存碎片

分为两个阶段:标记+整理

在这里插入图片描述

在这里插入图片描述

再来看第二个垃圾回收算法:标记整理,分为两个阶段:标记+整理,标记整理和标记清除算法在第一个阶段是一样的,也是
先对对象进行一个标记,看看哪些对象是垃圾,区别主要在第二步上,整理避免了之前标记清除时内存碎片的问题,整理会在
清除的过程中,把可用的对象向前移动,让内存更为紧凑,避免内存碎片的产生。整理之后发现内存更紧凑了,连续的空间更
多了,这样就不会造成标记清除算法内存碎片的问题,
复制代码

标记整理算法优缺点:

  • 优点

    • 避免了内存碎片的产生
  • 缺点

    • 速度慢

      由于整理牵扯到了对象的移动,效率会变得较低,对象在整理过程中要移动,移动的过程中如果有一些局部变量
      引用了存活的对象,肯定需要改变引用的引用地址,对象在内存中的位置变了,地址变了,肯定设计的工作就比
      较多一些,前者到内存区块的拷贝移动,还要把所有引用的地址加以改变。
      总结:干的活比较多,速度慢一些。
      复制代码

2.3 复制

定义:Copy

  • 不会有内存碎片
  • 需要占用双倍的内存空间

标记+复制+清理(FROM)+交换

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

最后学习一种垃圾回收算法:复制算法。复制算法比较特殊一些,它是把内存区域划成了大小相等的两块区域,
左边的称为FROM,右边的称之为TO,其中TO这个区域始终空闲着,里面一个对象都没有,那它是怎么做垃圾回收的呢,
它也是首先做一次标记,找到那些不被引用的对象标记为垃圾,然后从FROM区域上把FROM区域上还存活的对象复制到
TO区域中,复制的过程中就会完成碎片的整理,也是不会产生碎片,等复制完成可以看到FROM区域全是垃圾了(这里指的是
FROM区域的内存空间都当作空白内存用了),都没用了,一下子清空,并且交换FROM和TO它俩的位置,原来的TO变成了FROM,
原来的FROM变成了TO,TO总是空闲的一块空间。
复制代码

复制算法优缺点:

  • 优点
    • 不会产生内存碎片
  • 缺点
    • 复制算法会占用双倍的内存空间

上面所讲的三种算法实际在JVM的垃圾回收机制中都会根据不同的情况来采用,不会说只用其中一种算法,它是结合多种算法来共同实现垃圾回收的。

3. 分代垃圾回收

在这里插入图片描述

  • 对象首先分配在伊甸园区域

  • 新生代空间不足时,触发 Minor GC,伊甸园和幸存区From存活的对象使用复制算法复制到幸存区To中,存活的对象年龄加1并且交换幸存区From和幸存区To

  • Minor GC 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

    stop the world 就是在发生垃圾回收的时候必须暂停其它的用户线程,由垃圾回收线程来完成垃圾回收的动作,
    当把伊甸园和From中的对象拷贝到To后,把垃圾回收的动作做完了,其它的的用户线程才能继续运行。为什么垃圾
    回收的时候会把其它用户的线程暂停掉呢,这是因为垃圾回收的过程中牵扯到了对象的复制,也就是对象地址会发生
    改变,这种情况下如果多个线程大家都在同时运行,这样的话就会造成混乱,对象都移动了其他的线程再根据原来的
    地址访问这个对象,就找不到了,所以 Minor GC 在工作的过程中会引发 stop the world,也就是暂停其它用
    户线程,让垃圾回收这个线程先工作,等垃圾回收完了,其他的用户线程才恢复运行。Minor Gc 暂停时间非常的短,
    因为新生代本身大部分对象都是垃圾,都会被回收掉,复制的对象只有很少的一部分,所以暂停、复制的时间并不长,
    所以新生代触发的 stop the world 时间较短。
    复制代码
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4 bit)

    寿命是保存在每个对象的对象头中,其中存寿命的那个部分是4个bit,也就是4个0/1,4位最大能表示的数字是1111,
    转换成10进制也就是15,为什么不设的更大一些呢,一是没有意义,二是对象头那个部分比较金贵,每个byte都有各
    自的用途,不能把所有的空间都留给寿命存储寿命。虽然阈值最大是15,但是在不同的垃圾回收器的阈值也不一样,有
    的时候当空间紧张时,也许这个对象寿命还没有到15,它也会提前晋升到老年代中去,所以这里的15只是一个最大的阈
    值,并不是说一定要等到15才晋升到老年代,这是关于晋升的一个理解。
    复制代码
  • 当老年代空间不足,会先尝试触发 Minor GC,如果之后空间仍不足,那么触发 Full GC,

    当新生代垃圾回收以后,老年代的空间仍然不足以分配一些晋升对象时,这时候就会触发一次 Full GC,Full GC
    也会引起 stop the world,只不过相对于新生代的 Minor GC 引发的 stop the world,STW 的时间更长,
    为什么说它的时间更长呢,因为老年代它采用的回收算法跟新生代是不一样的,新生代那边是一种复制算法,而老年
    代因为存活对象比较多,整理起来、清除起来就比较慢,另外它采用的算法可能是标记+清除或者是标记+整理,标记+
    清除还能好点,速度会快一些,标记+整理速度会比较的慢,还有老年代的对象不是那么容易被当成垃圾进行回收,回收
    效率更低,STW 的时间更长。如果 Full GC 以后老年代的空间还是不够,这时候就触发 Out of Memory Error,
    堆内存溢出。
    复制代码

具体细节流程:

在这里插入图片描述

实际的jvm虚拟机不是单独采用其中一种算法,而是三种算法结合起来使用的,让它们协同工作,具体的实现就是虚拟机里的
分代垃圾回收机制。它把整个堆内存大的区域划成了两块,一个叫新生代,一个叫老年代,新生代了又进一步划分为三个小的
区域,分别是伊甸园、幸存区FROM、幸存区TO。那我们来思考一下,它为什么要做这样的一个区域划分呢,主要是因为java
中有的对象它可能需要长时间使用,长时间使用的对象就把它放到老年代当中,而那些用完了就可以丢弃的对象把它放在新生
代当中,这样的话就可以针对对象生命周期的不同特点进行不同的垃圾回收策略,老年代的垃圾回收很久才发生一次,而新生
代的垃圾回收发生的比较频繁,新生代处理的都是那些朝生夕死的对象,而老年代处理的都是那些更有价值会长时间存活的对
象,这样针对不同的区域采用不同的算法就可以更有效地对垃圾回收进行一个管理。打一个比方,现在有一栋居民楼,这个居
民楼就类比于java中的堆内存,然后居民楼中每家每户每天都要产生一些垃圾,这个垃圾需要保洁工人来进行一个处理,那
这个垃圾是隔多久来进行一个回收呢,显然如果这个保洁工人每家每户挨家挨户去收集垃圾,这个显然是比较耗时的,因为一
栋楼很多住户,每个住户都跑一遍,太累了,就好比对每个堆内存都扫描一遍,显然这个效率和速度是不可接受的,那现实生
活中是怎么处理的呢,会在楼下设一个专门丢弃垃圾的垃圾厂,这个就好比是一个新生代,这个垃圾厂里面的垃圾都是那些生
命周期更短的那些垃圾,比如每天吃完的盒饭,每天用完的手纸等等,都是垃圾场中回收更为频繁的垃圾,这个保洁工人只需
要每天打扫一次就可以了,而每家每户里存储的垃圾,可以看成处在一个老年代,这个垃圾比如说家里面用旧的椅子(不想扔
但是没用了),把它暂存在家里面,等到将来空间实在紧张,屋子东西摆不下的时候,再让保洁员进行一次大清理,把这些无
用的垃圾清理掉,当然这个耗时(很长时间才清理一次)就比较长了,执行的频率也比较低,因为垃圾场每天清理一次就够了,
老年代对应着那些老旧的垃圾都相对更有价值一些,它们只需要等到整个内存空间不足时,再去清理就可以了。这就是对分代
垃圾回收的理解。
复制代码

下面我们来详细看看分代垃圾回收机制是怎么工作的。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

当我们创建一个新的对象时,这个新的对象默认情况下会采用伊甸园的一块空间,这也很形象嘛,在西方神话中,伊甸园是
人类始祖亚当、夏娃它们诞生的地方,我们的对象也是诞生在伊甸园当中,接下来可能会有更多的对象被创建,然后它们都
分配到了伊甸园当中,伊甸园逐渐逐渐就被占满了,当再要创建一个对象时,发现伊甸园这个空间已经不够了,容纳不下了,
这个时候就会触发一次垃圾回收了,新生代的垃圾回收一般我们有一个称呼,叫做Minor GC,小的一次垃圾回收,Minor GC
触发了以后,会采用可达性分析算法沿着GC Root引用链去找,看这些对象是有用还是可以作为垃圾先进行一次标记的动作,
标记成功了会采用一种复制算法把存活的对象复制的幸存区TO中,复制过去以后会让幸存的对象寿命加1,刚开始寿命是0,
现在经历了一次垃圾回收还幸存下来,它们的寿命就会加1,做完复制动作后会交换幸存区From和幸存区To的位置,第一次
垃圾回收以后伊甸园空间充足了,可以继续向伊甸园分配新的对象,刚才放不下的对象就放进去了,又经过了一段时间分配
对象时伊甸园又满了,这时怎么办呢,再触发第二次垃圾回收,第二次垃圾回收除了要把伊甸园中存活的对象找到以外,还
需要看一看幸存区中有没有需要继续存活的对象,幸存区中的对象经历了第一次垃圾回收不死,不一定第二次就仍然存活,
也许第二次回收时它已经没用了,第二次垃圾回收的时候会把伊甸园中幸存的对象放到幸存区To中去,并且对象寿命加1,
把幸存区From中存活的对象也移到幸存区To中,幸存区From中存活的对象移到幸存区To中的对象寿命就变成了2,剩下一些
剩下一些垃圾对象(伊甸园和幸存区From中的垃圾)就回收掉,就把伊甸园空出来了,并且幸存区From和幸存区To要交换
位置,幸存区中的对象不会永远在幸存区待着,当它的寿命超过了一个阈值,默认的阈值是15,只要经历了15次垃圾回收
还活着,说明这个对象价值比较高,经常在使用,那没必要一直在幸存区留着了,为什么呢,你在幸存区留着以后再垃圾回收
还是不能会收你,那怎么办呢,一旦寿命超过阈值,就把它晋升到老年代去,因为老年代的垃圾回收频率比较低,不会轻易地
把它回收掉,这种价值较高的对象就从幸存区把它晋升到老年代去。这就是 Minor GC 附带的垃圾回收的流程,
复制代码

在这里插入图片描述

有这么一种场景:新生代放的对象挺多了,老年代放的对象也挺多了,几乎全满了,这时候会发生什么呢,比如说又来了一个对象,
往新生代放不下了(当然会触发一次新生代的内存回收),这时候就会触发一次 Full GC ,一般这些垃圾回收动作都是在空间不足
时才会触发,Full GC 的含义是当老年代的空间不足,老年代的垃圾回收,老年代的垃圾回收会做一次整个的清理,从新生到老年,
整个的进行一个清理,这就是垃圾回收过程中一个基本的流程。
复制代码

3.1 相关 VM 参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC-XX:+ScavengeBeforeFullGC
-Xms	堆的初始大小
-Xmx 或 -XX:MaxHeapSize=size		堆的最大大小
-Xmn 指定新生代的初始大小和最大大小

-XX:SurvivorRatio=ratio  幸存区比例(固定的) 
默认是8,就是新生代假设是10m内存,那么其中8m是划给伊甸园的,剩下的2m两等份,
一份为From,一份为To。8是指伊甸园的占比。

在某些垃圾回收器下需要动态的调整幸存区的比例,就不用自己去控制它是811还是622,
通过-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy参数
一个是初始化的幸存区比例,后面是个开关,开关打开了就会动态的调整新生代中伊甸园跟
幸存区的比例了。

下面的 -XX:MaxTenuringThreshold=threshold 表示最大的晋升阈值,寿命如果超过了阈值
就会从新生代晋升到老年代去,默认和垃圾回收器有关。有的垃圾回收器是15,有的垃圾回收器
是6,具体得结合具体的垃圾回收器来看。

-XX:+PrintTenuringDistribution 打印晋升的详情,如果对象发生了晋升,它会把它打印出来。

-XX:+PrintGCDetails -verbose:gc 发生垃圾回收时会打印详情信息。

最后的-XX:+ScavengeBeforeFullGC 意思是在做 Full GC 是不是要在新生代做一次
Minor GC 减少一些不必要的对象,加速 Full GC 的进度,这个开关打开意思就是在
Full GC 前先做一次 Minor GC,一般这样是有好处的,默认也是打开的。
复制代码

接下来我们通过一个案例来演示整个垃圾回收的过程并且要学会去读懂垃圾回收的日志。

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

public class Demo05 {

    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
    // 初始、最大的堆空间都是20m 新生代划了10m SerialGC是一种垃圾回收器(幸存区比例不会动态调整)
    // -XX:+PrintGCDetails -verbose:gc是打印 GC 的详情
    public static void main(String[] args) {

    } 
}
复制代码

我们运行上面代码:

在这里插入图片描述

在没有执行任何代码时伊甸园已经占用了27%(为什么什么也没干,伊甸园就占了27%呢,因为程序要运行,肯定需要一些对象去保证程序的运行,这些对象是会占用一定的空间的)

接下来我们创建一个7MB的byte[]数组,显然伊甸园的8M放不下了,是不是肯定要触发一次垃圾回收啊

package Memory.GC;

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

public class Demo05 {

    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
    // 初始、最大的堆空间都是20m 新生代划了10m SerialGC是一种垃圾回收器(幸存区比例不会动态调整)
    // -XX:+PrintGCDetails -verbose:gc是打印 GC 的详情
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
    }
}
复制代码

在这里插入图片描述

发生GC时从哪能看出来它是什么类型的GC呢,从名字上看,如果是老年代发生的GC,称之为 Full GC,
如果是新生代发生的 GC,也就是 Minor GC,它就叫 GC,没有 Minor。
后面的 DefNew 也是代表发生在新生代,后面的2068K->653K(9216K)代表回收前的内存占用和回收后
的内存占用,以及这个区域的总大小。
0.0048809 secs代表垃圾回收耗费的时间
2068K->653K(19456K)整个堆的回收前的内存占用和回收后的内存占用,以及堆的总大小
0.0060536 secs代表从堆的角度看这次回收耗费的时间
可以看到回收后伊甸园、From、To它们的内存占用发生了一些变化,其中一些对象被放入了
From中(应该是放入了To,但是放入后From和To位置交换了,所以最终我们看到的是From占用了一些对象),
伊甸园有92%被占用了,显然是刚才7M的对象进入了伊甸园的区域,其他部分没有发生变化,
复制代码

接下来再放一个512K的byte[]数组:

package Memory.GC;

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

public class Demo05 {

    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
    // 初始、最大的堆空间都是20m 新生代划了10m SerialGC是一种垃圾回收器(幸存区比例不会动态调整)
    // -XX:+PrintGCDetails -verbose:gc是打印 GC 的详情
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
    }
}
复制代码

在这里插入图片描述

可以看到还是只触发了第一次的垃圾回收,伊甸园几乎被放满了,因为有了7M的对象,又有了512KB,
已经到了98%的占用了,From那里还是不动,To那还是0。
复制代码

如果再放512KB呢,这时候伊甸园肯定不够了,就会触发第二次垃圾回收

package Memory.GC;

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

public class Demo05 {

    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
    // 初始、最大的堆空间都是20m 新生代划了10m SerialGC是一种垃圾回收器(幸存区比例不会动态调整)
    // -XX:+PrintGCDetails -verbose:gc是打印 GC 的详情
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);
    }
}
复制代码
可以看到又触发了一次新的垃圾回收,这次新的垃圾回收把新生代的内存占用从8.5M减少到了516K,
总容量当然不变,堆的回收前的总容量和回收后的总容量可以看到几乎不变,也就是可回收的对象
已经没多少了,总容量还是19M,,因为老年代还有10M呢,看时间的时候主要是看最后的real,
real就是这次垃圾回收耗费的时间,可以看到回收后,伊甸园已经占用的很少了,From占了50%,
显然新生代已容纳不下所有的对象了,所以很多对象就已经晋升到了老年代,虽然它没等到15次,
因为内存实在紧张,它就不会看那个阈值了,一部分对象已经晋升到了老年代。%76说明肯定是把
大对象(7MB)晋升到了老年代。
复制代码

下面再来研究一个之前没有提过的在垃圾回收时的一种策略,称之为大对象直接晋升到老年代

package Memory.GC;

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

public class Demo05 {

    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
    // 初始、最大的堆空间都是20m 新生代划了10m SerialGC是一种垃圾回收器(幸存区比例不会动态调整)
    // -XX:+PrintGCDetails -verbose:gc是打印 GC 的详情
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
    }
}
复制代码

我们看这样一个场景,比如说往集合中加一个比较大的对象(8MB),这个8MB其实已经超过了伊甸园的总容量,包括From也放不下它,所以新生代计算了一遍发现这个对象新生代根本容纳不下它,这个时候在新生代里触发垃圾回收它也放不下这个8MB的对象,那怎办呢,它就有这么一种策略,发现老年代空间足够,它就会把这个大的对象直接晋升到老年代去,这种情况下不会触发垃圾回收

在这里插入图片描述

我们发现伊甸园还是27%,这是最初始的那个状态,老年代直接上升到了80%,并没有 
GC 的信息出现,说明大对象
在老年代空间足够但新生代空间肯定不够的情况下,它会直接晋升,就不会引起新生
代的 GC 了。
复制代码

放两个8MB的对象:

package Memory.GC;

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

public class Demo05 {

    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
    // 初始、最大的堆空间都是20m 新生代划了10m SerialGC是一种垃圾回收器(幸存区比例不会动态调整)
    // -XX:+PrintGCDetails -verbose:gc是打印 GC 的详情
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
        list.add(new byte[_8MB]);
    }
}
复制代码

最后我们再来看一个现象,我们放两个8MB的对象这种情况下会出现什么呢,会出现 OutOfMemoryError,因为新生代的空间不足以容纳8M对象,老年代也放不下了而这两个byte[]数组都是由 GC Root 所引用,所以不能释放,因此会造成内存溢出,内存溢出前它还会做一次最后的自救工作,先是触发一次新生代的垃圾回收,当然这次垃圾回收相当于 Full GC 间接触发的,Full GC 触发了新生代的垃圾回收,然后发现不够又触发了老年代的垃圾回收,最后两次努力都做完了,发现还是不够,只有抛出 java.lang.OutOfMemoryError: Java heap space 了,堆内存空间不足造成了这次内存溢出,

在这里插入图片描述

最后再介绍一个误区,如果上面的代码是运行在一个线程里,这个线程运行的时候会发现内存分配不够导致最后内存溢出,它会不会影响到主线程,让主线程最终结束呢,我们来试验一下

package Memory.GC;

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

public class Demo05 {

    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
    // 初始、最大的堆空间都是20m 新生代划了10m SerialGC是一种垃圾回收器(幸存区比例不会动态调整)
    // -XX:+PrintGCDetails -verbose:gc是打印 GC 的详情
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();

        System.out.println("sleep....");
        Thread.sleep(10000000000L);
    }
}
复制代码

我们可以看到主线程运行了sleep,然后主线程等着了,然后里面的线程开始运行,运行的时候发现内存不足,经过了一次 GC 和 Full GC 以后,OutOfMemoryError 了,但是这个时候我们的程序还是没有结束,如果我们的程序正常结束了的话,伊甸园只有17%用了,老年代用了8M,但是我们的主线程其实并没有结束,一个线程内的 OutOfMemoryError 不会导致整个java进程的结束。

在这里插入图片描述

在这里插入图片描述

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改