JVM工作原理与实战(二十五):堆的垃圾回收-垃圾回收算法

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介: JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了垃圾回收算法评价标准、标记清除算法、复制算法、标记整理算法、分代垃圾回收算法等内容。

一、垃圾回收算法介绍

垃圾回收算法在Java中起到了至关重要的作用,它的主要任务是自动管理内存,避免内存泄漏和垃圾堆积。那么,Java是如何实现垃圾回收的呢?简单来说,垃圾回收要做的有两件事:

  • 找到内存中存活的对象:这部分的任务主要是通过“标记”过程来完成。在Java中,所有的对象都由引用进行连接,如果一个对象没有任何引用指向它,那么它就被认为是不可达的(也就是死亡对象)。标记阶段就是通过遍历所有的对象,找出并标记那些被引用的对象,从而确定哪些对象仍然存活。
  • 释放不再存活对象的内存:这部分的任务主要是通过“清除”过程来完成。一旦标记阶段确定了哪些对象是存活,那么所有未被标记的对象就被认为是不可达的,它们的内存就可以被安全地释放,以便程序能再次利用这部分空间。

1.垃圾回收算法的历史和分类

自1960年起,John McCarthy首次提出了标记-清除算法(Mark Sweep GC),标志着垃圾回收算法的诞生。随后,1963年Marvin L. Minsky引入了复制算法(Copying GC),这两种算法成为后续垃圾回收算法的基础。在此基础上,垃圾回收算法不断发展,出现了标记-整理算法(Mark Compact GC)、分代GC(Generational GC)等优化版本。这些算法在实现垃圾回收的过程中,均致力于提高内存利用率,降低停顿时间,以满足不同应用场景的需求。

2.垃圾回收算法的评价标准

在Java中,垃圾回收通过独立的GC线程完成,但无论采用何种GC算法,都会存在需要暂停所有用户线程的阶段。这一过程被称为“Stop-The-World”(STW)。如果该过程过长,将对用户体验产生负面影响。因此,评价垃圾回收算法的优劣需基于以下三个关键标准:

  • 吞吐量:吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 / (执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高。比如:虚拟机总共运行了 100 分钟,其中GC花掉 1 分钟,那么吞吐量就是 99%。
  • 最大暂停时间:最大暂停时间指的是所有在垃圾回收过程中的STW(Stop The World)时间最大值。STW是垃圾回收过程中需要停止所有的用户线程的时间。最大暂停时间越短,用户使用系统时受到的影响就越短。
  • 堆使用效率:不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。

上述三种评价标准,堆使用效率、吞吐量,以及最大暂停时间不可兼得。一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。不同的垃圾回收算法,适用于不同的场景。

二、垃圾回收算法详解

1.标记清除算法

标记清除算法是垃圾回收中的一种基础算法,其核心思想分为两个阶段:标记阶段和清除阶段。

  • 标记阶段:在标记阶段,算法通过可达性分析,从GC Root(垃圾回收起始点)开始,通过引用链遍历出所有存活的对象。
  • 清除阶段:在清除阶段,算法从内存中删除未被标记的对象,即非存活对象。

标记清除算法的优点

  • 只需要在标记阶段给每个对象维护一个标记位,然后在清除阶段删除未被标记的对象即可。

标记清除算法的缺点

  • 内存碎片化问题:由于内存是连续的,当对象被删除后,内存中会出现许多细小的可用内存单元。如果需要分配一个较大的空间,这些小内存单元可能无法满足需求,导致内存浪费和碎片化。
  • 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表。在分配内存时,可能需要遍历整个链表才能找到合适的空闲内存空间,导致分配速度变慢。

尽管标记清除算法存在一些缺陷,但它仍广泛应用于垃圾回收的实现中。在许多情况下,通过与其他算法结合使用,可以克服其缺点并提高垃圾回收的效率和性能。

2.复制算法

复制算法是一种垃圾回收算法,其核心思想是将堆内存分为两个相同的空间,即From空间和To空间。在对象分配阶段,只能使用其中一个空间(通常是From空间)。在垃圾回收阶段,存活的对象被复制到另一个未使用的空间(To空间)。完成复制后,两个空间的角色互换,原先的From空间变成To空间,而原先的To空间变成新的From空间。

完整的复制算法执行过程如下:

  1. 将整个堆内存分割成两个等大的空间,即From空间和To空间。
  2. 在对象分配阶段,新创建的对象只能在From空间中分配。
  3. 当垃圾回收阶段开始时,所有从GC Root开始的存活对象将被复制到To空间中。
  4. 接着,将GC Root及其关联的对象也复制到To空间中。
  5. 最后,清理From空间中的所有对象,并将两个空间的名称互换。

案例:

将整个堆内存分割成两个等大的空间,即From空间和To空间。在对象分配阶段,新创建的对象只能在From空间中分配。

image.gif

当垃圾回收阶段开始时,所有从GC Root开始的存活对象将被复制到To空间中,接着,将GC Root及其关联的对象也复制到To空间中。

image.gif

清理From空间中的所有对象,并将两个空间的名称互换。

image.gif

复制算法的优点

  • 吞吐量高:由于只需要遍历一次存活对象并将其复制到To空间,因此性能较好。相比之下,标记-整理算法需要两次遍历过程,标记-清除算法不需要移动对象。
  • 不会发生内存碎片化:由于在复制后对象按顺序放置在To空间中,因此对象以外的区域都是可用空间,不存在内存碎片化问题。

复制算法的缺点

  • 内存使用效率低:由于每次只能使用一半的内存空间来创建对象,因此内存利用率相对较低。这意味着在其他算法可以处理更多对象时,复制算法可能会浪费一半的内存空间。

3.标记整理算法

标记整理算法也被称为标记压缩算法,旨在解决标记清除算法中容易出现的内存碎片化问题。其核心思想分为两个阶段:

  • 标记阶段:这个阶段与标记清除算法中的标记阶段类似。通过可达性分析,从GC Root开始,遍历引用链以标记所有存活的对象。
  • 整理阶段:在这一阶段,所有存活的对象被移动到堆内存的一端。这有助于消除内存碎片,并使内存空间连续可用。

案例:

将所有存活的对象移动到堆内存的一端。

image.gif

标记整理算法的优点

  • 内存使用效率高:整个堆内存都可以被充分利用,不会像复制算法那样只能使用半个堆内存。
  • 防止内存碎片化:由于在整理阶段将存活对象移动到堆的一侧,剩余的空间可以连续地用于分配新对象,从而避免了内存碎片化问题。

标记整理算法的缺点

  • 整理阶段的效率问题:虽然存在一些高效的整理算法(如Two-Finger、表格算法、ImmixGC等),但标记整理算法的整体性能可能不如复制算法或标记清除算法。例如,某些实现可能需要遍历整个堆中的对象多次,这可能导致性能瓶颈。

为了提高标记整理算法的性能,可以采用一些优化策略。例如,使用更高效的标记和整理算法,或者结合其他垃圾回收算法(如复制算法或分代收集算法)来提高整体效率。通过合理的算法选择和优化,可以更好地平衡垃圾回收的效率和内存使用效率。

4.分代垃圾回收算法

分代垃圾回收算法是一种优秀的垃圾回收算法,它将整个内存区域划分为年轻代和老年代,以更高效地管理内存中的对象。这种算法通过将不同生命周期的对象划分到不同的区域,来优化垃圾回收的效率和性能。

image.gif

分代回收时,新创建的对象首先会被放入Eden伊甸园区。随着在Eden区中对象数量的增加,如果Eden区已满,新创建的对象将无法放入,此时会触发年轻代的GCMinor GCYoung GC)。Minor GC会回收Eden区From区需要回收的对象,并将未被回收的对象放入To区

随后,S0会变成To区,S1变成From区。当Eden区再次满时,继续往里放入对象,会再次触发Minor GC。这次会回收Eden区和S1(From)中的对象,并将Eden和From区中剩余的对象放入S0。

在每次Minor GC中,都会为对象记录年龄,初始值为0,每次GC后加1。如果Minor GC后对象的年龄达到阈值(最大15,默认值与垃圾回收器有关),该对象将被晋升至老年代

当老年代中空间不足,无法放入新的对象时,会先尝试Minor GC。如果仍然无法满足空间需求,就会触发Full GC。Full GC会对整个堆进行垃圾回收。如果Full GC仍然无法回收老年代中的对象,当继续尝试放入对象时,就会抛出Out Of Memory异常

案例:

分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。

image.gif

当Eden区满时,Minor GC或Young GC会被触发。Minor GC会回收Eden区和From区中的对象,并将未被回收的对象放入To区。

image.gif

S0会变成To区,S1变成From区(未被回收的对象放在From区)。当Eden区再次满时,Minor GC会被触发,回收Eden区和S1(from)中的对象,并将剩余对象放入S0区。每次Minor GC会记录对象的年龄。

image.gif

Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。

image.gif

当老年代空间不足,Minor GC后仍然无法放入新对象时,会触发Full GC,对整个堆进行垃圾回收。如果Full GC无法回收老年代的对象,当继续放入新对象时,会抛出Out Of Memory异常。

image.gif

在JDK 8中,可以使用-XX:+UseSerialGC参数来启用分代回收的垃圾回收器,并在运行程序时使用Arthas工具来查看分代之后的内存情况。通过Arthas的memory命令,可以显示出三个区域的内存情况,包括年轻代、老年代和元空间。

调整内存区域的大小:

参数名 参数含义
-Xms 设置堆的最小和初始大小,必须是1024倍数且大于1MB
-Xmx 设置最大堆的大小,必须是1024倍数且大于2MB
-Xmn 新生代的大小
-XX:SurvivorRatio=

伊甸园区和幸存区的比例,默认为8;

案例:新生代1g内存,伊甸园区800MB,S0和 S1各100MB

-XX:+PrintGCDetails verbose:gc 打印GC日志

案例(JDK 8中):

public static void main(String[] args) throws IOException {
        List<Object> list = new ArrayList<>();
        int count = 0;
        while (true){
            System.in.read();
            System.out.println(++count);
            list.add(new byte[1024 * 1024 * 1]);
        }
    }
image.gif

调整内存区域的大小:

-XX:+UseSerialGC  -Xms60m -Xmn20m -Xmx60m -XX:SurvivorRatio=3  -XX:+PrintGCDetails
image.gif

image.gif

通过Arthas的memory命令,查看三个区域的内存情况:

image.gif
eden_space 伊甸园区
survivor_space 幸存区
tenured_gen 老年代

通过合理地调整内存区域的大小和配置参数,可以更好地平衡垃圾回收的效率和内存使用效率。分代垃圾回收算法的应用广泛,是一种有效的垃圾回收策略,适用于各种应用场景。


总结

JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了垃圾回收算法评价标准、标记清除算法、复制算法、标记整理算法、分代垃圾回收算法等内容,希望对大家有所帮助。

相关文章
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
1050 55
|
Prometheus 监控 算法
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
本文介绍了CMS(Concurrent Mark-Sweep)垃圾回收器的工作原理、优缺点及常见问题,并通过具体案例分析了其优化策略。重点探讨了CMS的各个阶段,包括标记、并发清理和重标记
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
|
存储 算法 Java
G1原理—5.G1垃圾回收过程之Mixed GC
本文介绍了G1的Mixed GC垃圾回收过程,包括并发标记算法详解、三色标记法如何解决错标漏标问题、SATB如何解决错标漏标问题、Mixed GC的过程、选择CollectSet的算法
G1原理—5.G1垃圾回收过程之Mixed GC
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
存储 监控 架构师
ZGC圣经:ZGC垃圾回收器的原理、调优,ZGC 漏标的 分析与 研究
ZGC圣经:ZGC垃圾回收器的原理、调优,ZGC 漏标的 分析与 研究
|
存储 缓存 算法
G1原理—3.G1是如何提升垃圾回收效率
本文深入探讨了G1垃圾回收器提升GC效率的核心机制,包括记忆集(RSet)、位图(BitMap)和卡表(CardTable)的设计与作用。记忆集通过记录跨代引用避免了不必要的老年代遍历,位图用于高效描述内存使用状态以优化标记过程,而卡表则在节约记忆集内存的同时提供更详细的引用信息。此外,文章还解析了DCQ(Dirty Card Queue)和DCQS(Dirty Card Queue Set)机制如何异步更新RSet,确保在高并发场景下的性能与准确性。这些设计共同提升了G1在标记、清理及整理内存时的效率。
837 10
|
缓存 算法 Java
JVM深入原理(八)(一):垃圾回收
弱引用-作用:JVM中使用WeakReference对象来实现软引用,一般在ThreadLocal中,当进行垃圾回收时,被弱引用对象引用的对象就直接被回收.软引用-作用:JVM中使用SoftReference对象来实现软引用,一般在缓存中使用,当程序内存不足时,被引用的对象就会被回收.强引用-作用:可达性算法描述的根对象引用普通对象的引用,指的就是强引用,只要有这层关系存在,被引用的对象就会不被垃圾回收。引用计数法-缺点:如果两个对象循环引用,而又没有其他的对象来引用它们,这样就造成垃圾堆积。
318 0
|
算法 Java 对象存储
JVM深入原理(八)(二):垃圾回收
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为StopTheWorld简称STW,如果STW时间过长则会影响用户的使用。一般来说,堆内存越大,最大STW就越长,想减少最大STW,就会减少吞吐量,不同的GC算法适用于不同的场景。分代回收算法将整个堆中的区域划分为新生代和老年代。--超过新生代大小的大对象会直接晋升到老年代。
319 0
|
存储 算法 Java
G1原理—6.G1垃圾回收过程之Full GC
本文详细探讨了G1垃圾回收器对Full GC(FGC)的优化处理,涵盖FGC的前置处理、整体流程及并行化改进。重点分析了传统FGC串行化的局限性以及G1通过Region分区和RSet机制实现并行标记的优势,包括任务窃取提升效率、跨分区压缩以生成空闲Region等技术细节。此外,文章还介绍了G1的新特性——字符串去重优化,通过判断char数组一致性减少重复字符串占用内存,从而提升内存使用效率。总结部分全面回顾了G1在FGC中的各项优化措施及其带来的性能改善。
G1原理—6.G1垃圾回收过程之Full GC
|
存储 算法 Java
G1原理—4.G1垃圾回收的过程之Young GC
本文详细解析了G1垃圾回收器中YGC(Young Generation Collection)的完整流程,包括并行与串行处理阶段。内容涵盖YGC相关参数设置、YGC与Mixed GC及FGC的关系、新生代垃圾回收的具体步骤(如标记存活对象、复制到Survivor区、动态调整Region数量等),以及并行阶段的多线程操作和串行阶段的关键任务(如处理软引用、整理卡表、重构RSet)。
G1原理—4.G1垃圾回收的过程之Young GC