Java垃圾回收讲解

2025-03-08 02:30
404
0

在 Java 中,垃圾回收(Garbage Collection,简称 GC)是一项非常重要的机制,它帮助开发者自动管理内存,避免手动内存管理带来的复杂性和潜在错误。本文主要介绍 垃圾回收的基础原理、分代收集机制和不同JDK版本中的回收机制。

一、垃圾回收的基础原理

Java 采用自动内存管理机制,这意味着开发者不需要手动分配和释放内存。当创建一个对象时,Java 虚拟机(JVM)会自动为该对象分配内存;而当对象不再被使用时,垃圾回收器会自动回收这些对象所占用的内存。

如何判断一个对象是“垃圾”?Java通过可达性分析实现。

1.1、可达性分析

Java 垃圾回收器使用可达性分析算法来确定哪些对象是 “垃圾”,即哪些对象不再被使用,可以被回收。该算法的基本思想是从一组被称为根对象(GC Roots)的对象开始,通过引用关系遍历对象图,能够被GC Roots直接或间接引用到的对象被认为是 “可达” 的,而那些无法被 GC Roots 引用到的对象则被认为是 “不可达” 的,这些不可达对象就是垃圾对象,可以被回收。

常见的GC Roots包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象:例如方法中的局部变量所引用的对象。
  • 方法区中类静态属性引用的对象:例如类的静态变量所引用的对象。
  • 方法区中常量引用的对象:例如字符串常量池中的字符串对象。
  • 本地方法栈中 JNI(Java Native Interface)引用的对象:例如本地方法中引用的 Java 对象。

对上面虚拟机栈、方法区不理解的可以查看我的另一篇文章介绍Java的虚拟机结构

通过可达性Java可以判断哪些对象是“垃圾”了,但GC不止于此,就比如一个人的房间,如果东西胡乱摆放,清理会越来越困难,如果能分类常用的放哪里、不常用的放哪里,清理起来就会更方便。

1.2、分代收集理论

分代收集理论是 Java 垃圾回收的重要基础,它基于两个分代假说:

  • 弱分代假说:绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过越多次垃圾回收过程的对象就越难以消亡。

根据这两个假说,Java 堆被划分为不同的区域,主要包括新生代和老年代。新生代用于存放新创建的对象,这些对象通常很快就会变成垃圾;老年代用于存放存活时间较长的对象。不同代的对象采用不同的垃圾回收算法,以提高垃圾回收的效率。

内存分代模型:

区域 说明
新生代 存放新创建的对象,分为 Eden 区和两个 Survivor 区(S0, S1)
老年代 存放长期存活的对象(经过多次GC仍存活的对象)
元空间 存放类元数据(Java 8+,替代了永久代,使用本地内存)

1.3、垃圾回收算法

对标记为可回收的对象进行回收,释放其所占用的内存空间。常见的清除算法有:

  • 标记 - 清除算法(Mark-Sweep):先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象。该算法的缺点是会产生大量的内存碎片。(CMS使用)
  • 标记 - 整理算法(Mark-Compact):先标记出所有需要回收的对象,然后将所有存活的对象向一端移动,最后直接清理掉端边界以外的内存。该算法解决了内存碎片的问题,但移动对象的成本较高。(Serial/Parallel 的老年代)
  • 标记 - 复制算法(Mark-Copy):将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。该算法的缺点是可用内存空间减少了一半。(Serial/Parallel/G1 的年轻代)

二、分代收集机制

分代收集算法会根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

  • 新生代:对象的存活率较低,适合使用复制算法。新生代又分为 Eden 区和两个 Survivor 区(From Survivor 和 To Survivor),默认比例为 8:1:1。
  • 老年代:对象的存活率较高,适合使用标记 - 清除算法或标记 - 整理算法。

当创建一个新对象时,JVM 会根据对象的大小和类型,将其分配到不同的内存区域。通常,新创建的对象会被分配到新生代的 Eden 区。当Eden区满时就会触发新生代的垃圾回收(Minor GC)。

2.1、新生代垃圾回收(Minor GC)

随着新对象的不断创建,Eden 区会逐渐被填满。当Eden区满时,会触发一次Minor GC。Minor GC的主要流程如下:

  • 标记阶段:使用可达性分析算法标记出 Eden 区和 Survivor 区中存活的对象。
  • 复制阶段:将存活的对象复制到另一个 Survivor 区(假设为 Survivor1 区),同时清空 Eden 区和原来的 Survivor 区(假设为 Survivor0 区)。
  • 晋升阶段:如果某个对象在 Survivor 区中经历了一定次数(默认15)的 Minor GC 仍然存活,它将被晋升到老年代。

如果 Eden 区和 Survivor 区都无法容纳某个大对象,这个对象会被直接分配到老年代。

老年代主要用于存放存活时间较长的对象,其垃圾回收频率相对较低,回收过程也相对复杂。

2.2、老年代的垃圾回收(Major GC)

Major GC 是指发生在老年代的垃圾回收过程,主要用于回收老年代中的垃圾对象,释放内存空间。当老年代空间不足时触发Major GC。

不同的回收器策略不同,老年代常用的垃圾回收算法包括:

  • 标记-清除-整理(Mark-Sweep-Compact):标记无用对象,清除后整理内存碎片(如Serial Old、Parallel Old)。
  • 并发标记清除(CMS):通过并发阶段减少停顿时间(但存在内存碎片问题)。
  • G1的混合回收:G1收集器会同时处理年轻代和老年代的分区。

特点

  • 停顿时间较长:老年代对象多,标记和整理过程耗时。
  • 频率较低:对象晋升到老年代的速度通常较慢。

2.3、完全回收(Full GC)

Full GC 是指对整个堆(包括年轻代、老年代)以及方法区(Metaspace/PermGen)的垃圾回收。某些情况下还会触发其他区域的回收(如堆外内存或类卸载)。

触发条件:

  • 显式调用:通过 System.gc() 触发(不推荐,可能被JVM忽略)。
  • 老年代空间不足:Major GC后仍无法满足分配需求。
  • 元空间(Metaspace)不足:类元数据占用过多。
  • 晋升失败:年轻代对象晋升到老年代时,老年代空间不足。
  • 堆外内存(Direct Memory)不足:如NIO使用的内存。

回收行为:

Full GC 会暂停所有应用线程(Stop-The-World, STW),对堆内存进行全面回收。

具体算法取决于垃圾收集器:

  • Serial/Parallel收集器:使用单线程/多线程的标记-整理算法。
  • CMS收集器:退化为单线程的Serial Old收集器(失去并发优势)。
  • G1收集器:退化为单线程的Full GC(与Serial Old类似)。

特点:

  • 全局性影响:回收范围广,停顿时间最长。
  • 性能杀手:频繁Full GC会导致应用吞吐量下降和延迟飙升。

三、不同 JDK 版本中的常见回收机制

随着JDK版本的发展,GC的机制也一直在演进优化,如下介绍了各个JDK版本可能使用到的GC机制:

GC 机制 哪些 JDK 使用 描述 适用场景
串行垃圾回收器(Serial GC) JDK 1.2 - JDK 17 单线程进行垃圾回收,进行时会暂停所有用户线程。新生代用复制算法,老年代用标记 - 整理算法 单 CPU 环境下的小型应用程序
并行垃圾回收器(Parallel GC) JDK 1.2 - JDK 17 新生代和老年代都使用多线程进行垃圾回收,新生代采用复制算法,老年代采用标记 - 整理算法。JDK 7 及以后支持自适应调节策略,JDK 9 起成为默认 GC 对吞吐量要求较高、对停顿时间要求不是特别苛刻的应用程序,如批量数据处理任务
并发标记清除垃圾回收器(CMS GC) JDK 1.4 - JDK 1.8 以获取最短回收停顿时间为目标,采用标记 - 清除算法。过程分为初始标记、并发标记、重新标记、并发清除,初始和重新标记需 STW。(JDK 9 标记为废弃,JDK 14 移除。) 对响应时间要求较高的应用程序,如 Web 应用
G1 垃圾回收器(Garbage First) JDK 7u4 - JDK 17 将堆内存划分为多个大小相等的 Region,新生代和老年代不再物理隔离。采用标记 - 整理和复制算法,优先回收垃圾最多的 Region 大内存、多 CPU 的服务器应用,可同时满足高吞吐量和低停顿要求
ZGC(Z Garbage Collector) JDK 11 - JDK 17 可扩展的低延迟垃圾回收器,采用染色指针和读屏障技术,能在短时间完成回收,停顿时间几乎可忽略,支持 TB 级堆内存 对响应时间要求极高、内存使用量大的应用,如大型数据库、实时数据分析系统
Shenandoah GC JDK 12 - JDK 17 追求低延迟的垃圾回收器,通过与用户线程并发执行来减少停顿时间 对低延迟有严格要求的应用

JDK 7 及以前:

  • Serial GC:最基本、发展历史最悠久的收集器,它是一个单线程的收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到它回收结束。它采用复制算法,主要用于新生代的垃圾回收。
  • Parallel Scavenge GC:也是一个新生代收集器,使用复制算法,它是并行的多线程收集器,主要目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。
  • CMS(Concurrent Mark Sweep)GC:以获取最短回收停顿时间为目标的收集器,它是基于标记 - 清除算法实现的,主要用于老年代的垃圾回收。它的工作过程分为初始标记、并发标记、重新标记和并发清除四个阶段,其中初始标记和重新标记需要暂停用户线程,并发标记和并发清除可以与用户线程并发执行。

JDK 8:

  • JDK 8 默认使用的是 Parallel Scavenge GC(新生代)和 Parallel Old GC(老年代)的组合。Parallel Old GC是 Parallel Scavenge GC的老年代版本,使用标记 - 整理算法。

JDK 9 及以后:

  • G1(Garbage First):从 JDK 9 开始成为默认的垃圾收集器。G1 收集器将整个 Java 堆划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离的,而是一部分 Region 的集合。G1 收集器跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。它采用标记 - 整理算法和复制算法,整体上看是标记 - 整理算法,局部(两个 Region 之间)是复制算法。

四、附录

Java官网垃圾回收基础教程:https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

Java SE 17垃圾回收调优介绍:https://docs.oracle.com/en/java/javase/17/gctuning/introduction-garbage-collection-tuning.html

 

 

全部评论