欢迎光临
我们一直在努力

JVM垃圾回收机制面试题

1. 垃圾回收基础理论

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

详细解答:

垃圾回收定义

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

为什么需要GC

手动内存管理的问题:

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

GC的优势:

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

GC的核心问题

三个基本问题:

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

  • 2. 对象存活判断算法

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

    详细解答:

    引用计数法(Reference Counting)

    原理:

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

    优点:

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

    致命缺陷:循环引用

    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对象包括:

  • 虚拟机栈中引用的对象
  • public void method() {
    Object obj = new Object(); // obj是GC Root
    }

  • 方法区中类静态属性引用的对象
  • public class Test {
    public static Object staticObj = new Object(); // staticObj是GC Root
    }

  • 方法区中常量引用的对象
  • public class Test {
    public static final Object CONST_OBJ = new Object(); // CONST_OBJ是GC Root
    }

  • 本地方法栈中JNI引用的对象
  • native void nativeMethod(); // native方法中引用的对象

  • JVM内部引用
    • 基本类型对应的Class对象
    • 异常对象
    • 系统类加载器
  • synchronized持有的对象

  • 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)

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

    软引用(Soft Reference)

    SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);

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

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

    弱引用(Weak Reference)

    WeakReference<Object> weakRef = new WeakReference<>(new Object());

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

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

    虚引用(Phantom Reference)

    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);

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

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

    引用强度比较:

    强引用 > 软引用 > 弱引用 > 虚引用

    对象的自我拯救

    finalize()方法机制:

    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收集器的老年代回收

    内存碎片问题示例:

    // 假设需要分配连续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%)

    代码示例:

    // 新生代对象分配
    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策略(移动存活对象)

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

    2. Slide策略(滑动压缩)

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

    优点:

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

    缺点:

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

    应用场景:

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

    分代收集理论

    分代假说(Generational Hypothesis):

  • 弱分代假说:

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

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

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

    堆内存
    ├── 新生代(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
    • 回收范围:新生代+老年代+元空间
    • 停顿时间:最长

    对象晋升规则:

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

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

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

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

    架构师实战经验:

    分代收集优化要点:

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

  • 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 → [继续]

    单线程回收

    参数配置:

    -XX:+UseSerialGC # 新生代Serial + 老年代Serial Old

    适用场景:

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

    ParNew收集器

    特点:

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

    工作流程:

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

    参数配置:

    -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)

    关键参数:

    -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%

    自适应策略:

    // JVM自动调整:
    新生代大小
    EdenSurvivor比例
    晋升老年代对象年龄阈值

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

    适用场景:

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

    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停顿
    ▒ = 并发执行

    参数配置:

    -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)

    // 并发标记阶段产生的新垃圾
    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(记忆集)

    // 记录Region之间的引用关系
    // 避免全堆扫描

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

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

    2. Collection Set(回收集合)

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

    3. 停顿预测模型

    // 基于历史数据预测回收时间
    // 动态选择回收Region数量

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

    参数配置:

    -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)

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

    3. 并发整理

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

    参数配置:

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

    适用场景:

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

    架构师选择建议:

    场景推荐收集器理由
    小堆(<2G)低延迟 ParNew+CMS 成熟稳定
    中大堆(4-64G) G1 可预测停顿
    超大堆(>64G)极低延迟 ZGC 停顿时间<10ms
    批处理高吞吐量 Parallel 吞吐量最高
    赞(0)
    未经允许不得转载:171主机测评 » JVM垃圾回收机制面试题
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址