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

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

相关文章
|
6天前
|
机器学习/深度学习 算法 机器人
多代理强化学习综述:原理、算法与挑战
多代理强化学习是强化学习的一个子领域,专注于研究在共享环境中共存的多个学习代理的行为。每个代理都受其个体奖励驱动,采取行动以推进自身利益;在某些环境中,这些利益可能与其他代理的利益相冲突,从而产生复杂的群体动态。
32 5
|
4天前
|
算法 Java 开发者
Java中的垃圾回收机制:从原理到实践
Java的垃圾回收机制(Garbage Collection, GC)是其语言设计中的一大亮点,它为开发者提供了自动内存管理的功能,大大减少了内存泄漏和指针错误等问题。本文将深入探讨Java GC的工作原理、不同垃圾收集器的种类及它们各自的优缺点,并结合实际案例展示如何调优Java应用的垃圾回收性能,旨在帮助读者更好地理解和有效利用Java的这一特性。
|
10天前
|
搜索推荐 Shell
解析排序算法:十大排序方法的工作原理与性能比较
解析排序算法:十大排序方法的工作原理与性能比较
30 9
|
8天前
|
机器学习/深度学习 算法
机器学习入门(三):K近邻算法原理 | KNN算法原理
机器学习入门(三):K近邻算法原理 | KNN算法原理
|
26天前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
8天前
|
前端开发 Java 应用服务中间件
JVM进阶调优系列(1)类加载器原理一文讲透
本文详细介绍了JVM类加载机制。首先解释了类加载器的概念及其工作原理,接着阐述了四种类型的类加载器:启动类加载器、扩展类加载器、应用类加载器及用户自定义类加载器。文中重点讲解了双亲委派机制,包括其优点和缺点,并探讨了打破这一机制的方法。最后,通过Tomcat的实际应用示例,展示了如何通过自定义类加载器打破双亲委派机制,实现应用间的隔离。
|
1月前
|
监控 算法 Java
掌握Java的垃圾回收机制:从原理到实践
在Java的世界中,垃圾回收(Garbage Collection,简称GC)是一块神秘的领域,它如同一位默默无闻的清洁工,确保内存中不再使用的对象得到妥善处理。本文将带你走进垃圾回收的大门,探索它的工作原理、常见算法及其在实际应用中的调优策略。无论你是初学者还是有一定经验的开发者,这篇文章都将为你揭开垃圾回收的神秘面纱,让你的Java程序运行得更加高效和稳定。
54 5
|
1月前
|
机器学习/深度学习 算法 Python
群智能算法:深入解读人工水母算法:原理、实现与应用
近年来,受自然界生物行为启发的优化算法备受关注。人工水母算法(AJSA)模拟水母在海洋中寻找食物的行为,是一种新颖的优化技术。本文详细解读其原理及实现步骤,并提供代码示例,帮助读者理解这一算法。在多模态、非线性优化问题中,AJSA表现出色,具有广泛应用前景。
|
2月前
|
前端开发 算法 JavaScript
React原理之Diff算法
【8月更文挑战第24天】
|
2月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。