Interview AiBox logo

Interview AiBox 实时 AI 助手,让你自信应答每一场面试

download免费下载
进阶local_fire_department12 次面试更新于 2025-09-03account_tree思维导图

请介绍Java中常见的垃圾回收算法及其原理。

lightbulb

题型摘要

Java垃圾回收是JVM自动管理内存的核心机制,主要算法包括标记-清除、标记-复制、标记-整理和分代收集算法。标记-清除算法简单但会产生内存碎片;标记-复制算法高效但内存利用率低;标记-整理算法解决碎片问题但移动对象耗时;分代收集算法根据对象生命周期将内存划分为新生代和老年代,采用不同回收策略,是目前主流方案。Java提供了多种垃圾收集器如Serial、ParNew、Parallel Scavenge、CMS、G1等,选择时需考虑应用特点、内存大小和CPU资源等因素。

Java中常见的垃圾回收算法及其原理

1. 垃圾回收的基本概念和目的

垃圾回收(Garbage Collection, GC)是Java虚拟机自动管理内存的核心机制,其主要目的是自动回收不再使用的对象所占用的内存,避免内存泄漏和内存溢出问题,减轻开发人员的内存管理负担。

2. 判断对象是否可回收的方法

在介绍具体的垃圾回收算法之前,先了解如何判断对象是否可回收:

2.1 引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器为0的对象就是不可能再被使用的。这种方法实现简单,效率高,但很难解决对象之间相互循环引用的问题。

2.2 可达性分析算法

通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在Java中,可作为GC Roots的对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

3. 常见的垃圾回收算法及其原理

3.1 标记-清除算法(Mark-Sweep)

标记-清除算法是最基础的垃圾回收算法,分为"标记"和"清除"两个阶段:

  1. 标记阶段:从GC Roots开始遍历,标记所有可达的对象。
  2. 清除阶段:遍历整个堆,将未被标记的对象回收。

优点

  • 实现简单,易于理解

缺点

  • 效率不高:标记和清除两个过程的效率都不高
  • 空间碎片:标记清除后会产生大量不连续的内存碎片,可能导致后续需要分配较大对象时,无法找到足够的连续内存而提前触发另一次垃圾收集。
--- title: 标记-清除算法示意图 --- graph TD A["堆内存"] --> B["标记前"] A --> C["标记后"] A --> D["清除后"] B --> B1["存活对象"] B --> B2["垃圾对象"] B --> B3["存活对象"] B --> B4["垃圾对象"] C --> C1["已标记存活对象"] C --> C2["未标记垃圾对象"] C --> C3["已标记存活对象"] C --> C4["未标记垃圾对象"] D --> D1["存活对象"] D --> D2["空闲空间"] D --> D3["存活对象"] D --> D4["空闲空间"]

3.2 标记-复制算法(Mark-Copy)

为了解决标记-清除算法的效率问题,标记-复制算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点

  • 实现简单,运行高效
  • 不会产生内存碎片

缺点

  • 内存利用率低:可用内存缩小为原来的一半
  • 对象存活率较高时,复制操作效率会降低
--- title: 标记-复制算法示意图 --- graph TD A["堆内存"] --> B["复制前"] A --> C["复制后"] B --> B1["From空间"] B1 --> B11["存活对象"] B1 --> B12["垃圾对象"] B1 --> B13["存活对象"] B1 --> B14["垃圾对象"] B --> B2["To空间"] B2 --> B21["空闲"] C --> C1["From空间"] C1 --> C11["空闲"] C --> C2["To空间"] C2 --> C21["存活对象"] C2 --> C22["存活对象"] C2 --> C23["空闲"]

3.3 标记-整理算法(Mark-Compact)

标记-整理算法的标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

优点

  • 不会产生内存碎片
  • 不需要额外空间

缺点

  • 移动对象并更新所有引用这些对象的地方,相对耗时
--- title: 标记-整理算法示意图 --- graph TD A["堆内存"] --> B["标记前"] A --> C["标记后"] A --> D["整理后"] B --> B1["存活对象"] B --> B2["垃圾对象"] B --> B3["存活对象"] B --> B4["垃圾对象"] B --> B5["存活对象"] C --> C1["已标记存活对象"] C --> C2["未标记垃圾对象"] C --> C3["已标记存活对象"] C --> C4["未标记垃圾对象"] C --> C5["已标记存活对象"] D --> D1["存活对象"] D --> D2["存活对象"] D --> D3["存活对象"] D --> D4["空闲空间"]

3.4 分代收集算法(Generational Collection)

分代收集算法是目前主流商业虚拟机采用的垃圾回收算法。它根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代(Young Generation)和老年代(Old Generation)。

基本思想

  • 新生代:每次垃圾收集时都有大批对象死去,只有少量存活,使用"标记-复制"算法
  • 老年代:对象存活率高,没有额外空间对它进行分配担保,使用"标记-清除"或"标记-整理"算法

工作原理

  1. 新创建的对象首先在新生代的Eden区分配
  2. 当Eden区没有足够空间时,触发Minor GC
  3. 存活的对象被复制到Survivor区,每次Minor GC,对象的年龄会增加
  4. 当对象年龄达到一定阈值(默认为15),被晋升到老年代
  5. 当老年代空间不足时,触发Major GC或Full GC
--- title: 分代收集算法示意图 --- graph TD A["Java堆"] --> B["新生代 Young Generation"] A --> C["老年代 Old Generation"] B --> B1["Eden区 80%"] B --> B2["Survivor区0 10%"] B --> B3["Survivor区1 10%"] B1 --> D["新创建的对象"] B2 --> E["从Eden或Survivor1复制过来的存活对象"] B3 --> F["从Eden或Survivor0复制过来的存活对象"] C --> G["长期存活的对象"] C --> H["大对象直接进入老年代"] D --> I["Minor GC"] I --> J["存活对象复制到Survivor区"] J --> K["对象年龄增加"] K --> L["年龄达到阈值"] L --> M["晋升到老年代"] G --> N["Major GC/Full GC"]

4. Java中常见的垃圾收集器及其实现的算法

4.1 Serial收集器

Serial收集器是最基本、历史最悠久的收集器,它是一个单线程收集器,在进行垃圾收集时,必须暂停其他所有的工作线程。

特点

  • 单线程
  • 使用"标记-复制"算法(新生代)和"标记-整理"算法(老年代)
  • 适用于Client模式下的虚拟机

4.2 ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为与Serial收集器完全一样。

特点

  • 多线程
  • 使用"标记-复制"算法(新生代)
  • 是许多运行在Server模式下的虚拟机中首选的新生代收集器

4.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用"标记-复制"算法的并行多线程收集器。

特点

  • 多线程
  • 使用"标记-复制"算法
  • 目标是达到一个可控制的吞吐量(Throughput)

4.4 Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用"标记-整理"算法。

特点

  • 单线程
  • 使用"标记-整理"算法
  • 主要意义在于给Client模式下的虚拟机使用

4.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法。

特点

  • 多线程
  • 使用"标记-整理"算法
  • 注重吞吐量及CPU资源利用率

4.6 CMS(Concurrent Mark Sweep)收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,基于"标记-清除"算法实现。

特点

  • 并发收集
  • 使用"标记-清除"算法
  • 运行过程分为四个步骤:
    1. 初始标记(CMS initial mark)
    2. 并发标记(CMS concurrent mark)
    3. 重新标记(CMS remark)
    4. 并发清除(CMS concurrent sweep)

缺点

  • 对CPU资源敏感
  • 无法处理浮动垃圾(Floating Garbage)
  • 产生大量空间碎片

4.7 G1(Garbage-First)收集器

G1收集器是当今收集器技术发展的最前沿成果之一,它是一款面向服务端应用的垃圾收集器。

特点

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿
  • 将整个Java堆划分为多个大小相等的独立区域(Region),并跟踪每个Region里的垃圾价值
  • 优先回收价值最大的Region(这也是Garbage-First名称的由来)
--- title: G1收集器Region划分示意图 --- graph TD A["Java堆"] --> B["Region 1 Eden"] A --> C["Region 2 Eden"] A --> D["Region 3 Survivor"] A --> E["Region 4 Old"] A --> F["Region 5 Old"] A --> G["Region 6 Humongous"] A --> H["Region 7 Eden"] A --> I["Region 8 Old"] B --> J["新生代Eden区"] C --> J H --> J D --> K["新生代Survivor区"] E --> L["老年代Old区"] F --> L I --> L G --> M["大对象Humongous区"]

5. 垃圾回收算法的选择和优化

选择合适的垃圾回收算法需要考虑以下因素:

  1. 应用特点

    • 吞吐量优先(Throughput First):适合后台计算、数据处理等任务
    • 停顿时间优先(Pause Time First):适合Web应用、交互式应用等对响应时间要求高的场景
  2. 堆内存大小

    • 小堆内存:适合使用Serial或ParNew等简单收集器
    • 大堆内存:适合使用CMS或G1等并发收集器
  3. CPU资源

    • CPU资源充足:可以使用并行或并发收集器
    • CPU资源紧张:适合使用串行收集器
  4. 对象生命周期

    • 大量短生命周期对象:适合使用分代收集算法
    • 长生命周期对象较多:可能需要调整老年代的比例和收集策略
  5. JDK版本

    • JDK 8及之前:常用Parallel Scavenge + CMS组合
    • JDK 9及之后:G1成为默认收集器
    • JDK 11及之后:引入ZGC和Shenandoah等低延迟收集器

优化建议

  • 根据应用特点选择合适的垃圾收集器
  • 合理设置堆内存大小和新生代与老年代的比例
  • 调整垃圾回收的触发阈值和频率
  • 监控垃圾回收的性能指标,如吞吐量、停顿时间等
  • 避免显式调用System.gc()

总结

Java中的垃圾回收算法主要包括标记-清除、标记-复制、标记-整理和分代收集算法。不同的算法有各自的优缺点,适用于不同的场景。Java虚拟机提供了多种垃圾收集器,如Serial、ParNew、Parallel Scavenge、CMS、G1等,它们实现了不同的垃圾回收算法。选择合适的垃圾回收算法和收集器需要考虑应用特点、堆内存大小、CPU资源等因素。在实际应用中,需要根据具体情况进行调优,以达到最佳的性能表现。

参考资料:

account_tree

思维导图

Interview AiBox logo

Interview AiBox — 面试搭档

不只是准备,更是实时陪练

Interview AiBox 在面试过程中提供实时屏幕提示、AI 模拟面试和智能复盘,让你每一次回答都更有信心。

AI 助读

一键发送到常用 AI

Java垃圾回收是JVM自动管理内存的核心机制,主要算法包括标记-清除、标记-复制、标记-整理和分代收集算法。标记-清除算法简单但会产生内存碎片;标记-复制算法高效但内存利用率低;标记-整理算法解决碎片问题但移动对象耗时;分代收集算法根据对象生命周期将内存划分为新生代和老年代,采用不同回收策略,是目前主流方案。Java提供了多种垃圾收集器如Serial、ParNew、Parallel Scavenge、CMS、G1等,选择时需考虑应用特点、内存大小和CPU资源等因素。

智能总结

深度解读

考点定位

思路启发

auto_awesome

相关题目

请解释AQS(AbstractQueuedSynchronizer)是什么,它的核心原理和应用场景是什么?

AQS(AbstractQueuedSynchronizer)是Java并发包中的核心抽象类,用于构建锁和同步器。其核心原理包括:1)使用volatile int state管理同步状态;2)使用CLH队列变种管理等待线程;3)支持独占和共享两种模式;4)大量使用CAS操作保证原子性。AQS的应用场景广泛,包括构建ReentrantLock等锁、Semaphore等同步器,以及ThreadPoolExecutor等并发工具。理解AQS对掌握Java并发编程至关重要。

arrow_forward

请解释Spring框架中的三级缓存机制及其在解决循环依赖问题中的作用。

Spring框架中的三级缓存机制是解决循环依赖问题的核心设计。它包括三个Map缓存:一级缓存(singletonObjects)存储完全初始化的单例Bean;二级缓存(earlySingletonObjects)存储提前暴露但未完全初始化的Bean;三级缓存(singletonFactories)存储Bean工厂对象。当发生循环依赖时,Spring通过三级缓存提前暴露未完全初始化的Bean实例,使相互依赖的Bean能够完成初始化。此机制仅适用于单例Bean的setter注入方式,无法解决构造器注入和原型/多例Bean的循环依赖问题。

arrow_forward

什么是多态?

多态是面向对象编程的核心特性之一,指"同一接口,多种实现",允许不同对象对同一消息做出不同响应。多态分为编译时多态(方法重载)和运行时多态(方法重写),以及重载、参数、子类型和强制四种类型。多态提高了代码复用性、灵活性和可扩展性,降低了类之间的耦合度。实现多态通常需要继承、封装和抽象等OOP特性的支持。

arrow_forward

请解释HashMap和Hashtable的区别与联系

HashMap和Hashtable都是Java中实现Map接口的类,用于存储键值对映射。主要区别在于:HashMap是非线程安全的,允许null键和值,性能较高;Hashtable是线程安全的,不允许null键和值,性能较低。HashMap继承自AbstractMap,使用fail-fast迭代器,默认初始容量为16,扩容为2倍;Hashtable继承自Dictionary(已过时),使用非fail-fast的Enumerator,默认初始容量为11,扩容为2倍+1。两者都基于哈希表实现,提供快速查找。在现代Java开发中,HashMap通常是首选,除非有特殊线程安全需求,此时ConcurrentHashMap通常是比Hashtable更好的选择。

arrow_forward

请详细解释抽象类和接口的区别,以及它们在Java中的适用场景。

抽象类和接口是Java中实现抽象的两种机制。抽象类使用abstract关键字定义,可包含抽象方法和具体方法,支持单继承,适合代码复用和维护状态;接口使用interface关键字定义,Java 8后可包含默认方法,支持多实现,适合定义契约和解耦。抽象类表示"is-a"关系,接口表示"can-do"关系。选择时应考虑是否需要代码复用、状态维护、多重继承或契约定义等因素。

arrow_forward