在 Java 开发中,垃圾回收(Garbage Collection,简称 GC)是一个既熟悉又陌生的概念。它像一位隐形的清洁工,在程序运行时默默清理不再使用的内存资源,让开发者无需手动管理内存,却又在关键时刻影响着系统的性能表现。本文将从底层原理到实际应用,全面剖析 Java 垃圾回收机制的核心逻辑与实践要点。
Java 垃圾回收的本质是自动释放不再被引用的对象所占用的内存空间,其核心目标有三:
与 C/C++ 等语言的手动内存管理相比,Java 的垃圾回收机制在安全性上有显著优势,但这并不意味着开发者可以完全忽视内存管理 —— 不合理的对象创建与引用方式,依然可能引发内存溢出(OOM)或频繁 GC 导致的性能问题。
垃圾回收的第一步是准确判断哪些对象已经 "死亡"(即不再被引用)。目前主流的判断算法有两种:
这是一种简单直观的实现:给每个对象添加一个引用计数器,每当有地方引用它时,计数器加 1;当引用失效时,计数器减 1。当计数器为 0 时,认为该对象可被回收。
但这种算法存在一个致命缺陷 ——无法解决循环引用问题。例如:
public class ReferenceCountDemo { Object instance = null; public static void main(String[] args) { ReferenceCountDemo a = new ReferenceCountDemo(); ReferenceCountDemo b = new ReferenceCountDemo(); a.instance = b; b.instance = a; a = null; b = null; // 此时a和b互相引用,计数器不为0,无法被回收 }}
正因为这个缺陷,Java 虚拟机并没有采用引用计数法,而是使用了可达性分析算法。
该算法的核心思想是:以一系列称为 "GC Roots" 的对象为起点,向下搜索引用链(Reference Chain)。如果一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到该对象不可达),则证明此对象是不可用的。
在 Java 中,可作为 GC Roots 的对象包括:
可达性分析算法完美解决了循环引用问题,是目前 Java 虚拟机采用的主流判断方式。
即使通过可达性分析判定为不可达的对象,也并非立即被回收。一个对象的消亡需要经历以下过程:
需要注意的是,finalize()方法最多只会被系统自动调用一次,且其执行时间不确定,因此不建议在实际开发中依赖此方法进行资源释放,更推荐使用try-finally等方式。
确定了需要回收的对象后,接下来就是如何高效地回收内存。Java 虚拟机中主要的垃圾收集算法包括:
这是最基础的收集算法,分为 "标记" 和 "清除" 两个阶段:
该算法的缺点很明显:
为解决标记 - 清除算法的效率问题,复制算法应运而生。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。
这种算法的优点是:
缺点是:
目前 Java 虚拟机中的新生代收集器(如 Serial、ParNew)大多采用这种算法,不过实际实现中会将内存划分为一块较大的 Eden 空间和两块较小的 Survivor 空间(比例通常为 8:1:1),每次使用 Eden 和其中一块 Survivor,这样内存利用率可达 90%。
复制算法在对象存活率较高时会频繁进行复制操作,效率大打折扣。针对老年代对象存活率高的特点,标记 - 整理算法应运而生:
这种算法避免了内存碎片,同时也不需要牺牲一半内存空间,但整理过程需要移动大量对象,成本较高。
当前商业虚拟机的垃圾收集都采用 "分代收集" 算法,其核心思想是根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代:
分代收集算法并不是一种新的算法,而是结合了前面几种算法的优点,根据不同代的特点选择最合适的收集方式。
垃圾收集器是垃圾回收算法的具体实现,不同的 Java 虚拟机实现可能提供不同的垃圾收集器。HotSpot 虚拟机中常见的垃圾收集器包括:
Serial 收集器是最基本、发展历史最悠久的收集器,它是一个单线程收集器,在进行垃圾收集时,必须暂停其他所有工作线程("Stop The World"),直到收集结束。
优点:简单高效,对于单个 CPU 环境,Serial 收集器由于没有线程交互的开销,单线程收集效率最高。
缺点:收集过程中会产生长时间的停顿,不适合现代服务器环境。
ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为与 Serial 收集器基本一致。
ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,因为它是除了 Serial 收集器外,唯一能与 CMS 收集器配合工作的收集器。
Parallel Scavenge 收集器也是新生代收集器,使用复制算法,又是多线程收集器。它的特点是关注吞吐量(Throughput,即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值)。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
Parallel Scavenge 收集器提供了两个参数来精确控制吞吐量:
Serial Old 是 Serial 收集器的老年代版本,使用标记 - 整理算法,单线程收集,主要用于 Client 模式下的虚拟机。
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用标记 - 整理算法,多线程收集。在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 的组合。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,使用标记 - 清除算法,主要针对老年代收集。
CMS 收集器的工作过程分为四个步骤:
CMS 收集器的优点是并发收集、低停顿,但也存在以下缺点:
G1(Garbage-First)收集器是面向服务端应用的垃圾收集器,是 JDK 9 及以上版本的默认收集器。它具有以下特点:
G1 收集器将 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,而是一部分 Region 的集合。
理解垃圾回收机制的最终目的是解决实际问题,以下是一些常见的 GC 调优原则和实践建议:
GC 调优的目标通常有两个:
这两个目标往往相互矛盾,需要根据业务场景进行权衡。
通常可以通过以下参数设置:
有效的 GC 调优需要基于实际监控数据,常用的监控工具包括:
Java 垃圾回收机制是 JVM 自动内存管理的核心,它通过复杂的算法和实现,为开发者屏蔽了内存管理的细节,同时也带来了一定的性能开销。作为 Java 开发者,理解垃圾回收的工作原理,掌握常见的调优方法,对于编写高性能、稳定的 Java 应用至关重要。
随着 JVM 技术的不断发展,垃圾回收机制也在持续优化,从早期的 Serial 收集器到现在的 G1 收集器,再到 ZGC、Shenandoah 等新一代低延迟收集器,Java 的垃圾回收能力不断提升。但无论技术如何发展,理解内存管理的本质,写出更符合 GC 友好性的代码,始终是开发者的核心能力。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。