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 吞吐量最高
相关推荐
jushisi2 小时前
下载eclipse MAT(Memory Analyzer Tool)
java·服务器
尽兴-2 小时前
JVM垃圾收集器与三色标记算法详解
java·jvm·算法·cms·gc·g1·三色标记算法
easyboot2 小时前
C#通过sqlsugar插入数据到postgresql
开发语言·c#
Anastasiozzzz2 小时前
Redis脑裂问题--面试坑点【Redis的大脑裂开?】
java·数据库·redis·缓存·面试·职场和发展
txinyu的博客2 小时前
C++ 线程库
开发语言·c++
木土雨成小小测试员2 小时前
Python测试开发之后端一
开发语言·数据库·人工智能·python·django·sqlite
tkevinjd2 小时前
4-初识Maven
java·maven
ckhcxy2 小时前
抽象类和接口(二)
java
superman超哥2 小时前
Serialize 与 Deserialize Trait:Rust 类型系统与序列化的完美融合
开发语言·rust·开发工具·编程语言·rust序列化·rust类型·serialize