JVM垃圾回收机制面试题

1. 垃圾回收基础理论

问题:什么是垃圾回收?为什么需要垃圾回收?

详细解答:

垃圾回收定义

垃圾回收(Garbage Collection,GC)是自动内存管理机制,负责识别和回收不再使用的对象所占用的内存空间。

为什么需要GC

手动内存管理的问题:

  • C/C++需要手动malloc/free,容易出现内存泄漏
  • 野指针问题导致程序崩溃
  • 双重释放造成内存损坏
  • 开发效率低,需要时刻关注内存释放

GC的优势:

  • 自动回收不再使用的对象
  • 避免内存泄漏和野指针
  • 提高开发效率
  • 程序更加健壮可靠

GC的核心问题

三个基本问题:

  1. 哪些内存需要回收? → 对象是否存活判断
  2. 什么时候回收? → GC触发条件
  3. 如何回收? → 垃圾回收算法

2. 对象存活判断算法

问题:如何判断一个对象是否可以被回收?

详细解答:

引用计数法(Reference Counting)

原理:

复制代码
对象添加引用计数器
引用+1:被引用时
引用-1:引用失效时
引用=0:可回收

优点:

  • 实现简单
  • 判定效率高
  • 实时性好(引用计数为0立即回收)

致命缺陷:循环引用

java 复制代码
public class CircularReference {
    public Object instance = null;
    
    public static void main(String[] args) {
        CircularReference obj1 = new CircularReference();
        CircularReference obj2 = new CircularReference();
        
        // 循环引用
        obj1.instance = obj2;
        obj2.instance = obj1;
        
        obj1 = null;
        obj2 = null;
        
        // 两个对象互相引用,引用计数永远不为0
        // 但实际上已经无法访问,造成内存泄漏
    }
}

Python的解决方案:

  • 使用引用计数 + 标记清除解决循环引用
  • JVM没有采用此方案

可达性分析算法(Reachability Analysis)

原理:

复制代码
从GC Roots对象作为起点向下搜索
搜索路径称为引用链(Reference Chain)
对象到GC Roots没有任何引用链相连 → 不可达 → 可回收

GC Roots对象包括:

  1. 虚拟机栈中引用的对象
java 复制代码
public void method() {
    Object obj = new Object();  // obj是GC Root
}
  1. 方法区中类静态属性引用的对象
java 复制代码
public class Test {
    public static Object staticObj = new Object();  // staticObj是GC Root
}
  1. 方法区中常量引用的对象
java 复制代码
public class Test {
    public static final Object CONST_OBJ = new Object();  // CONST_OBJ是GC Root
}
  1. 本地方法栈中JNI引用的对象
java 复制代码
native void nativeMethod();  // native方法中引用的对象
  1. JVM内部引用
  • 基本类型对应的Class对象
  • 异常对象
  • 系统类加载器
  1. synchronized持有的对象

  2. JMXBean、JVMTI中注册的回调、本地代码缓存等

可达性分析示例:

复制代码
GC Roots
  ├─> Object A ──> Object C
  ├─> Object B ──> Object D ──> Object E
  │
  └─> Object F

Object G <──> Object H  (互相引用但不可达)

结论:
- A, B, C, D, E, F 可达,存活
- G, H 不可达,可回收

引用类型详解

强引用(Strong Reference)

java 复制代码
Object obj = new Object();  // 强引用
// 只要强引用存在,永远不会被回收
// 宁可OOM也不回收

软引用(Soft Reference)

java 复制代码
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);

// 内存充足:不回收
// 内存不足:回收(OOM前)

// 应用场景:缓存
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();

弱引用(Weak Reference)

java 复制代码
WeakReference<Object> weakRef = new WeakReference<>(new Object());

// 无论内存是否充足,GC时一定回收
// 生命周期:下次GC前

// 应用场景:WeakHashMap
WeakHashMap<Key, Value> cache = new WeakHashMap<>();

虚引用(Phantom Reference)

java 复制代码
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);

// 无法通过虚引用获取对象
// 唯一目的:对象被回收时收到系统通知

// 应用场景:堆外内存回收(DirectByteBuffer)

引用强度比较:

复制代码
强引用 > 软引用 > 弱引用 > 虚引用

对象的自我拯救

finalize()方法机制:

java 复制代码
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    
    public void isAlive() {
        System.out.println("I am still alive!");
    }
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        // 自我拯救:重新建立引用
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        
        // 第一次拯救成功
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);  // 等待finalize执行
        
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();  // 输出:I am still alive!
        } else {
            System.out.println("I am dead!");
        }
        
        // 第二次拯救失败(finalize只执行一次)
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("I am dead!");  // 输出这个
        }
    }
}

finalize()的问题:

  • 执行时间不确定(低优先级Finalizer线程)
  • 性能开销大
  • 只执行一次
  • 可能导致对象复活

架构师建议:

  • 避免使用finalize()
  • 使用try-finally或try-with-resources
  • JDK 9引入Cleaner机制替代finalize()

3. 垃圾回收算法详解

问题:JVM中有哪些垃圾回收算法?各有什么优缺点?

详细解答:

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

工作流程:

复制代码
1. 标记阶段:标记所有需要回收的对象
2. 清除阶段:统一回收被标记的对象

示意图:

复制代码
回收前:[对象A][对象B][对象C][对象D][对象E]
标记后:[对象A][×对象B][对象C][×对象D][对象E]
清除后:[对象A][    ][对象C][    ][对象E]

优点:

  • 实现简单
  • 不需要移动对象

缺点:

  • 效率问题:标记和清除效率都不高
  • 空间问题:产生大量内存碎片

应用场景:

  • CMS收集器的老年代回收

内存碎片问题示例:

java 复制代码
// 假设需要分配连续100MB内存
byte[] largeArray = new byte[100 * 1024 * 1024];

// 虽然总空闲内存>100MB,但没有连续的100MB空间
// 导致分配失败,触发Full GC或OOM

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

工作流程:

复制代码
1. 将内存分为两块:From区和To区
2. 使用From区分配对象
3. GC时将From区存活对象复制到To区
4. 清空From区
5. 交换From和To的角色

示意图:

复制代码
From区:[A][B][C][D][E]  To区:[空]
  ↓ GC(B、D为垃圾)
From区:[空]  To区:[A][C][E]

HotSpot的Eden + Survivor实现:

复制代码
新生代分配:Eden:Survivor0:Survivor1 = 8:1:1

正常情况:
Eden + Survivor0 → Survivor1(存活对象<10%)

极端情况:
存活对象>Survivor容量 → 老年代担保分配

优点:

  • 实现简单
  • 运行高效
  • 没有内存碎片

缺点:

  • 空间浪费:可用内存缩小为原来的一半
  • 存活率高时效率降低(需要复制大量对象)

应用场景:

  • 新生代回收(对象存活率低,约10%)

代码示例:

java 复制代码
// 新生代对象分配
public class YoungGenAllocation {
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] allocation1 = new byte[2 * _1MB];  // Eden
        byte[] allocation2 = new byte[2 * _1MB];  // Eden
        byte[] allocation3 = new byte[2 * _1MB];  // Eden
        
        // Eden空间不足,触发Minor GC
        // allocation1、2、3晋升到老年代
        byte[] allocation4 = new byte[4 * _1MB];
    }
}

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

工作流程:

复制代码
1. 标记阶段:标记存活对象
2. 整理阶段:让所有存活对象向内存一端移动
3. 清理阶段:清理边界外的内存

示意图:

复制代码
标记后:[A][×B][C][×D][E][×F]
整理后:[A][C][E][          ]
        ↑存活对象  ↑可分配空间

两种实现策略:

1. Move策略(移动存活对象)

java 复制代码
// 伪代码
for (Object obj : liveObjects) {
    moveToCompactArea(obj);
    updateReferences(obj);  // 更新所有引用
}

2. Slide策略(滑动压缩)

java 复制代码
// 伪代码
// 三次扫描:
// 1. 计算新地址
// 2. 更新引用
// 3. 移动对象

优点:

  • 没有内存碎片
  • 空间利用率高
  • 适合老年代(存活率高)

缺点:

  • 效率问题:需要移动大量对象并更新引用
  • 暂停时间长(Stop The World)

应用场景:

  • Serial Old收集器
  • Parallel Old收集器

分代收集理论

分代假说(Generational Hypothesis):

  1. 弱分代假说:

    • 绝大多数对象都是朝生夕灭
    • 98%的对象在第一次GC后被回收
  2. 强分代假说:

    • 熬过多次GC的对象越难消亡
    • 长时间存活的对象生命周期会更长
  3. 跨代引用假说:

    • 跨代引用相对于同代引用占极少数
    • 存在互相引用关系的对象倾向于同时生存或消亡

分代设计:

复制代码
堆内存
├── 新生代(Young Generation)
│   ├── Eden区(80%)
│   ├── Survivor0区(10%)
│   └── Survivor1区(10%)
└── 老年代(Old Generation)

回收策略:

Minor GC(新生代GC):

  • 触发条件:Eden区满
  • 回收算法:复制算法
  • 频率:高(秒级)
  • 停顿时间:短(毫秒级)

Major GC(老年代GC):

  • 触发条件:老年代满或晋升失败
  • 回收算法:标记-清除或标记-整理
  • 频率:低(分钟-小时级)
  • 停顿时间:长(可能达到秒级)

Full GC(全堆GC):

  • 触发条件:
    • 老年代空间不足
    • 元空间不足
    • System.gc()调用
    • CMS GC出现promotion failed、concurrent mode failure
  • 回收范围:新生代+老年代+元空间
  • 停顿时间:最长

对象晋升规则:

java 复制代码
1. 长期存活对象进入老年代
   -XX:MaxTenuringThreshold=15  // 默认15次

2. 大对象直接进入老年代
   -XX:PretenureSizeThreshold=1048576  // 1MB

3. 动态年龄判定
   // Survivor空间中相同年龄所有对象大小总和 > Survivor空间一半
   // 年龄>=该年龄的对象直接进入老年代

4. 空间分配担保
   // Minor GC前检查老年代最大连续空间 > 新生代所有对象总大小
   // 是:安全执行Minor GC
   // 否:Full GC

架构师实战经验:

分代收集优化要点:

  1. 根据对象生命周期特征调整新生代大小
  2. 合理设置晋升阈值避免频繁Full GC
  3. 大对象使用对象池或直接分配到老年代
  4. 监控晋升速率评估内存配置合理性

4. 垃圾收集器详解

问题:JVM有哪些垃圾收集器?各有什么特点和适用场景?

详细解答:

收集器总览

复制代码
新生代收集器:
- Serial
- ParNew
- Parallel Scavenge

老年代收集器:
- Serial Old
- Parallel Old
- CMS

全堆收集器:
- G1
- ZGC(JDK 11)
- Shenandoah(JDK 12)

Serial / Serial Old收集器

特点:

  • 单线程收集器
  • 收集时必须暂停所有工作线程(Stop The World)
  • 简单高效(单线程下没有线程交互开销)

工作流程:

复制代码
用户线程 → [暂停] → Serial GC → [继续]
           ↓
        单线程回收

参数配置:

bash 复制代码
-XX:+UseSerialGC  # 新生代Serial + 老年代Serial Old

适用场景:

  • Client模式(桌面应用)
  • 单核CPU或内存较小的环境
  • 对停顿时间不敏感的应用

ParNew收集器

特点:

  • Serial的多线程版本
  • 新生代并行,老年代串行
  • 与CMS配合使用

工作流程:

复制代码
用户线程 → [暂停] → ParNew GC(多线程) → [继续]

参数配置:

bash 复制代码
-XX:+UseParNewGC           # 使用ParNew
-XX:ParallelGCThreads=4    # GC线程数(通常=CPU核心数)

线程数配置建议:

复制代码
CPU核心数 <= 8:GC线程数 = CPU核心数
CPU核心数 > 8:GC线程数 = 3 + (5 * CPU核心数 / 8)

适用场景:

  • 多核CPU环境
  • 配合CMS使用

Parallel Scavenge / Parallel Old收集器

特点:

  • 吞吐量优先收集器
  • 新生代和老年代都是并行回收
  • 自适应调节策略(GC Ergonomics)

关键参数:

bash 复制代码
-XX:+UseParallelGC              # 新生代Parallel Scavenge
-XX:+UseParallelOldGC           # 老年代Parallel Old
-XX:MaxGCPauseMillis=200        # 最大停顿时间(毫秒)
-XX:GCTimeRatio=99              # 吞吐量大小(默认99,即1%时间GC)
-XX:+UseAdaptiveSizePolicy      # 自适应调节策略

吞吐量计算:

复制代码
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间)

例如:
运行100分钟,GC 1分钟
吞吐量 = 100 / (100 + 1) = 99%

自适应策略:

java 复制代码
// JVM自动调整:
- 新生代大小
- Eden与Survivor比例
- 晋升老年代对象年龄阈值

// 目标:在停顿时间和吞吐量之间找到最优解

适用场景:

  • 后台计算任务(批处理、科学计算)
  • 不需要太多交互的应用
  • 对停顿时间不敏感但要求高吞吐量

CMS收集器(Concurrent Mark Sweep)

设计目标:

  • 获取最短停顿时间
  • 互联网站或B/S系统的服务端

工作流程(四个阶段):

1. 初始标记(Initial Mark)- STW

复制代码
标记GC Roots直接关联的对象
速度快,停顿时间短

2. 并发标记(Concurrent Mark)- 并发

复制代码
从GC Roots遍历整个对象图
与用户线程并发执行
时间最长但不停顿

3. 重新标记(Remark)- STW

复制代码
修正并发标记期间变动的对象标记记录
使用增量更新算法
停顿时间略长于初始标记

4. 并发清除(Concurrent Sweep)- 并发

复制代码
清除死亡对象
与用户线程并发执行

时间线:

复制代码
用户线程: ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
CMS GC:        ║  ▒▒▒▒▒▒  ║  ▒▒▒▒
            初始 并发标记 重新 并发清除
            标记         标记

║ = STW停顿
▒ = 并发执行

参数配置:

bash 复制代码
-XX:+UseConcMarkSweepGC                    # 使用CMS
-XX:CMSInitiatingOccupancyFraction=70      # 触发CMS的老年代占用阈值
-XX:+UseCMSInitiatingOccupancyOnly         # 只使用设定的阈值
-XX:+CMSScavengeBeforeRemark               # 重新标记前进行一次Minor GC
-XX:+UseCMSCompactAtFullCollection         # Full GC时进行碎片整理
-XX:CMSFullGCsBeforeCompaction=5           # 多少次Full GC后整理一次

优点:

  • 并发收集
  • 低停顿

缺点:

1. CPU资源敏感

复制代码
默认GC线程数 = (CPU核心数 + 3) / 4

4核CPU:1个GC线程,占用25% CPU
2核CPU:1个GC线程,占用50% CPU(影响严重)

2. 无法处理浮动垃圾(Floating Garbage)

java 复制代码
// 并发标记阶段产生的新垃圾
Object obj = new Object();  // 并发标记开始前存在
obj = null;                 // 并发标记期间变为垃圾

// 这部分垃圾要等下次GC才能回收

3. 内存碎片问题

复制代码
使用标记-清除算法
产生大量内存碎片
可能导致提前触发Full GC

4. Concurrent Mode Failure

复制代码
触发原因:
- 并发清除期间,老年代空间不足以容纳晋升对象
- 预留空间不足(CMSInitiatingOccupancyFraction设置过高)

后果:
- 启用Serial Old收集器进行Full GC
- 停顿时间大幅增加

解决方案:
- 降低CMSInitiatingOccupancyFraction值
- 增加老年代大小

适用场景:

  • 重视响应速度的应用
  • 互联网网站、B/S系统
  • 不能容忍长时间停顿的服务

G1收集器(Garbage First)

设计目标:

  • 在延迟可控的情况下获得尽可能高的吞吐量
  • 替代CMS收集器

核心概念:Region

复制代码
堆内存划分为多个大小相等的Region(1-32MB)

Region类型:
- Eden区
- Survivor区
- Old区
- Humongous区(大对象,>=Region大小的50%)

工作流程:

1. 初始标记(Initial Mark)- STW

复制代码
标记GC Roots直接关联的对象
借用Minor GC的暂停

2. 并发标记(Concurrent Mark)- 并发

复制代码
遍历对象图
使用SATB(Snapshot-At-The-Beginning)算法

3. 最终标记(Final Mark)- STW

复制代码
处理SATB缓冲区

4. 筛选回收(Live Data Counting and Evacuation)- STW

复制代码
根据停顿时间目标选择回收Region
将选中Region的存活对象复制到空Region
回收旧Region空间

关键技术:

1. Remembered Set(记忆集)

java 复制代码
// 记录Region之间的引用关系
// 避免全堆扫描

// 每个Region维护一个RSet
class Region {
    RememberedSet rset;  // 记录哪些Region引用了本Region的对象
}

// Minor GC时只需扫描:
// - Eden区
// - Survivor区
// - RSet记录的引用Region

2. Collection Set(回收集合)

复制代码
记录要被回收的Region集合
根据停顿时间目标动态选择
优先回收垃圾最多的Region(Garbage First)

3. 停顿预测模型

java 复制代码
// 基于历史数据预测回收时间
// 动态选择回收Region数量

预测因素:
- 每个Region的垃圾占比
- 历史回收耗时
- 复制存活对象的耗时

参数配置:

bash 复制代码
-XX:+UseG1GC                         # 使用G1
-XX:MaxGCPauseMillis=200             # 最大停顿时间目标
-XX:G1HeapRegionSize=16m             # Region大小
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发GC的堆占用阈值
-XX:G1NewSizePercent=5               # 新生代最小占比
-XX:G1MaxNewSizePercent=60           # 新生代最大占比
-XX:ParallelGCThreads=8              # 并行GC线程数
-XX:ConcGCThreads=2                  # 并发GC线程数

Mixed GC详解:

复制代码
触发条件:
1. 并发标记完成
2. 老年代占用达到阈值

回收范围:
- 整个新生代
- 部分老年代Region

选择策略:
根据停顿时间目标和垃圾占比选择最值得回收的Region

优点:

  • 可预测的停顿时间
  • 没有内存碎片(复制算法)
  • 并行与并发结合
  • 分代收集但不需要连续空间

缺点:

  • 内存占用高(RSet占堆内存约10%-20%)
  • 执行负载高(写屏障维护RSet)
  • 小堆(<4G)性能可能不如CMS

适用场景:

  • 大堆内存(>4G)
  • 需要可预测停顿时间
  • 替代CMS的生产环境

ZGC收集器(JDK 11+)

设计目标:

  • 停顿时间不超过10ms
  • 支持TB级堆内存
  • 停顿时间不随堆大小增加而增加

核心技术:

1. 着色指针(Colored Pointer)

复制代码
64位指针布局:
[18位未使用][1位Finalizable][1位Remapped]
[1位Marked1][1位Marked0][42位对象地址]

通过指针中的标志位标记对象状态

2. 读屏障(Load Barrier)

java 复制代码
// 每次从堆中读取对象引用时,检查并修复指针
Object obj = object.field;
// 读屏障检查指针状态
// 必要时进行重新映射

3. 并发整理

复制代码
使用转发表(Forwarding Table)
实现对象移动的并发

参数配置:

bash 复制代码
-XX:+UseZGC                    # 使用ZGC
-XX:ZCollectionInterval=120    # GC间隔(秒)
-XX:ZAllocationSpikeTolerance=2  # 分配尖峰容忍度

适用场景:

  • 大内存应用(>100G)
  • 要求极低延迟(<10ms)
  • JDK 11及以上版本

架构师选择建议:

场景 推荐收集器 理由
小堆(<2G)低延迟 ParNew+CMS 成熟稳定
中大堆(4-64G) G1 可预测停顿
超大堆(>64G)极低延迟 ZGC 停顿时间<10ms
批处理高吞吐量 Parallel 吞吐量最高
相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔4 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区9 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai10 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说10 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json