面试必备之jvm垃圾收集器

《从"垃圾"到"宝贝":JVM垃圾回收的奇妙冒险》

大家好!欢迎来到"码农脱口秀"现场,今天我们要聊的话题是------JVM垃圾回收。没错,就是那个让Java程序员又爱又恨的"清洁工"系统。它默默无闻地帮我们收拾内存残局,却总在关键时刻"抢镜"(GC停顿了解一下?)。准备好和面试官来一场关于"垃圾"的深度对话了吗?

第一幕:基础问答------垃圾回收的"垃圾分类"

面试官:嗨,小明!听说你最近在研究JVM?那你知道JVM为什么要进行垃圾回收吗?

小明(自信满满):当然!就像我家的老妈子总唠叨"不用的东西赶紧扔",JVM也需要定期清理不再使用的对象,不然内存迟早要"爆仓"!

面试官(微笑):很形象的比喻。那JVM怎么判断哪些对象是"垃圾"呢?

小明:主要有两大"鉴宝"技术:

  1. 引用计数法:给每个对象配个计数器,被引用就+1,引用失效就-1。归零就是垃圾。

// 伪代码示例

css 复制代码
 Object a = new Object();// a的引用计数=1

 Object b = a; // a的引用计数=2 

b = null; // a的引用计数=1

 a = null; // a的引用计数=0 → 可回收
  1. 但这种方法有个致命缺点------循环引用检测不出来:
ini 复制代码
class Node { Node next; } 

Node a = new Node(); // a计数=1 

Node b = new Node(); // b计数=1

 a.next = b; // b计数=2 

b.next = a; // a计数=2 

a = b = null; // a,b计数=1 → 内存泄漏!
  1. 可达性分析(JVM实际采用):从GC Roots(如栈帧中的局部变量、静态变量等)出发,像蜘蛛网一样扫描引用链,不在网里的就是垃圾。

面试官:不错!那GC Roots具体包括哪些?

小明(搬出手指开始数):

  1. 虚拟机栈中的引用(就是正在吃的方法参数、局部变量)
  2. 本地方法栈中的JNI引用
  3. 方法区中静态属性引用(static大哥们)
  4. 方法区中常量引用(final大佬们)
  5. 同步锁持有的对象(synchronized的保镖们)
  6. JMX等虚拟机内部引用

面试官:如果对象被判了"死刑",会立即执行吗?

小明:不会!它们还有一次"临终申诉"的机会------finalize()方法。但这个方法不靠谱(可能被卡住或复活对象),所以JDK9就被标记为废弃了。就像死刑犯突然说"我知道一个大秘密",然后...可能真就被暂停执行了:

csharp 复制代码
class Zombie {
    static Zombie saved;
    
    @Override
    protected void finalize() {
        System.out.println("救命!我不想死!");
        saved = this; // 把自己复活
    }
    
    public static void main(String[] args) throws Exception {
        Zombie z = new Zombie();
        z = null;
        System.gc(); // 第一次GC,触发finalize
        Thread.sleep(1000); // 给finalize执行时间
        System.out.println("幸存者:" + saved); // 非null
    }
}

第二幕:算法剖析------垃圾回收的"十八般武艺"

面试官:看来基础很扎实。那说说常见的垃圾收集算法吧?

小明(撸起袖子):主要有三大门派:

1. 标记-清除(Mark-Sweep)------"简单粗暴派"

less 复制代码
内存布局:
[可用][对象A][对象B][可用][对象C][对象D]

标记阶段:
A、C被引用,B、D是垃圾

清除后:
[可用][对象A][可用][可用][对象C][可用]
  • 优点:简单直接,老江湖了
  • 缺点:产生内存碎片,就像玩俄罗斯方块却不能紧凑排列

2. 标记-整理(Mark-Compact)------"强迫症患者"

ini 复制代码
整理前:
[对象A][垃圾][对象B][垃圾][垃圾][对象C]

整理后:
[对象A][对象B][对象C][可用][可用][可用]
  • 优点:内存整齐,适合老年代
  • 缺点:移动对象成本高,就像搬家要重新布置所有家具

3. 复制算法(Copying)------"土豪分家法"

ini 复制代码
From区:
[对象A][垃圾][对象B][垃圾]

复制存活对象到To区:
To区:[对象A][对象B]

然后清空From区
  • 优点:高效无碎片,适合"朝生暮死"的新生代
  • 缺点:内存利用率只有50%,像双十一备货------仓库永远有一半是空的

面试官:现代JVM好像不是简单用某一种?

小明 :没错!就像川菜会融合其他菜系,JVM也玩分代收集

  • 新生代(Eden+Survivor):用复制算法,因为这里对象"死亡率"高达98%
  • 老年代:用标记-清除或标记-整理,这里对象都是"老油条",存活率高

举个栗子

arduino 复制代码
// 模拟对象晋升过程
public class GenerationGC {
    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
        byte[] allocation4 = new byte[4 * _1MB]; // 直接进老年代(假设PretenureSizeThreshold=3MB)
    }
}

第三幕:收集器盘点------JVM的"清洁天团"

面试官:听说JVM有很多垃圾收集器,能介绍一下吗?

小明(掏出应援棒):来认识下这个"偶像天团"!

新生代"小鲜肉"组:

  1. Serial:单线程收集,工作时会"冻结世界"(Stop-The-World),但简单高效,适合客户端模式

使用方式 java -XX:+UseSerialGC MainClass

  1. ParNew:Serial的多线程版,CMS的御用新生代搭档

java -XX:+UseParNewGC MainClass

  1. Parallel Scavenge:吞吐量优先,适合后台运算

java -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 MainClass

老年代"实力派"组:

  1. Serial Old:Serial的老年代版,CMS的备胎
  2. Parallel Old:Parallel Scavenge的老年代搭档
  3. CMS(Concurrent Mark-Sweep):以最短停顿时间为目标
  • java -XX:+UseConcMarkSweepGC MainClass
  • 四步曲:初始标记→并发标记→重新标记→并发清除
  • 缺点:会产生"浮动垃圾",且内存碎片多
  1. G1(Garbage-First):面向服务端的全能选手
  • java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 MainClass
  • 特点:将堆划分为多个Region,优先回收价值高的Region
  • 适合大内存(6GB+)应用

ZGC与Shenandoah------"未来之星"

  • ZGC:JDK11引入,目标停顿<10ms,不管堆多大

java -XX:+UseZGC MainClass

  • Shenandoah:与ZGC类似,但贡献给OpenJDK

java -XX:+UseShenandoahGC MainClass

面试官:CMS和G1在并发阶段有什么区别?

小明(画图解释):

makefile 复制代码
CMS工作流程:
1. 初始标记(STW) → 2. 并发标记 → 3. 重新标记(STW) → 4. 并发清除

G1工作流程:
1. 初始标记(STW) → 2. 并发标记 → 3. 最终标记(STW) → 4. 筛选回收(STW)

关键区别:

  • CMS在"并发清除"时不压缩,导致碎片
  • G1的"筛选回收"会选择收益高的Region回收

第四幕:实战调优------给JVM"把脉开方"

面试官:假设我们有个电商系统,大促时频繁Full GC,怎么排查?

小明(戴上侦探帽):Follow me!

第一步:收集"犯罪现场"证据

ruby 复制代码
# 打印GC日志
java -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps MainClass

# 发生OOM时dump内存
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof MainClass

第二步:分析GC日志

scss 复制代码
[Full GC (Ergonomics) [PSYoungGen: 1024K->0K(2048K)] 
[ParOldGen: 4096K->4096K(8192K)] 5120K->4096K(10240K), 
[Metaspace: 2560K->2560K(1056768K)], 0.123456 secs]
  • 老年代(ParOldGen)回收前后都是4096K → 可能有内存泄漏

第三步:使用MAT分析堆转储

csharp 复制代码
// 典型内存泄漏示例
public class LeakExample {
    static List<byte[]> leak = new ArrayList<>();
    
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            leak.add(new byte[1_000_000]); // 每次1MB
            System.out.println("已添加:" + (i + 1) + "MB");
        }
    }
}

在MAT中查看Dominator Tree,找到占用最大的对象链。

排查思路

1、首先排查jvm参数配置

2、排查内存泄露情况 (可能的问题io等待,threadlocal内存泄露 , sql查询参数缺失,查询大量数据,内存不足)

3、创建的对象过大,直接放入老年代 (或者新生代空间过小,直接进入老年大)

4、元数据区空间不足,full gc 方法区的实现(1.7以前是永久代,1.8以后是元数据区)

调优"药方":

  1. 增大堆大小(简单粗暴):

java -Xms4g -Xmx4g MainClass

  1. 调整新生代比例

java -XX:NewRatio=2 # 老年代/新生代=2/1

  1. 设置晋升阈值

java -XX:MaxTenuringThreshold=15 # 对象熬过15次GC才晋升老年代

  1. 换用G1收集器

java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 MainClass

面试官:如果系统有大量长时间存活的缓存对象怎么办?

小明 :这时候可以考虑堆外内存软/弱引用

swift 复制代码
// 使用WeakHashMap做缓存(内存不足时自动回收)
Map<Key, Value> cache = new WeakHashMap<>();

// 或者更精细控制的软引用
Map<Key, SoftReference<Value>> softCache = new HashMap<>();

五、Java内存管理的"四重人格"

先来看一张"忍者能力表":

引用类型 GC时表现 是否影响对象生命周期 典型应用场景 类比现实
强引用 宁可OOM也不回收 普通对象创建 你的结婚戒指
软引用 内存不足时才回收 否(间接影响) 缓存 应急备用金
弱引用 发现即回收 临时缓存、WeakHashMap 临时工
虚引用 随时可能回收 资源清理跟踪 对象临终关怀护士

5.1. 强引用(Strong Reference)------"至死不渝型"

javascript 复制代码
Object obj = new Object(); // 这就是强引用

特点

  • 只要强引用存在,对象就永远不会被回收
  • 即使OOM也不放手(真·内存界的霸道总裁)

应用场景

  • 日常开发中99%的对象创建
  • 需要长期持有的核心对象

段子时刻

强引用就像你妈觉得你冷:"穿秋裤!就算内存爆炸你也得给我穿着!"

5.2. 软引用(Soft Reference)------"识时务者为俊杰型"

arduino 复制代码
SoftReference<byte[]> softRef = new SoftReference<>(new byte[10_000_000]);

特点

  • 内存充足时:岁月静好
  • 内存不足时:果断自保(在OOM前被回收)

实战代码(缓存实现):

typescript 复制代码
public class ImageCache {
    private final Map<String, SoftReference<Bitmap>> cache = new HashMap<>();
    
    public void add(String key, Bitmap image) {
        cache.put(key, new SoftReference<>(image));
    }
    
    public Bitmap get(String key) {
        SoftReference<Bitmap> ref = cache.get(key);
        return ref != null ? ref.get() : null;
    }
}

应用场景

  • 图片/资源缓存
  • 计算结果缓存
  • 任何"有挺好,没有也能重新算"的数据

冷知识

从JDK1.3.1开始,软引用对象会按最近使用时间排序,最后使用的会先被回收(LRU策略)。

5.3. 弱引用(Weak Reference)------"塑料兄弟情型"

php 复制代码
WeakReference<Employee> weakRef = new WeakReference<>(new Employee());

特点

  • 只要GC一运行,不管内存充不充足都回收
  • 比软引用更"薄情"

经典案例(WeakHashMap):

ini 复制代码
WeakHashMap<Employee, Salary> salaryMap = new WeakHashMap<>();
Employee bob = new Employee("Bob");
salaryMap.put(bob, new Salary(5000));

bob = null; // 没有强引用了
System.gc(); // GC后bob对应的条目会自动从map中消失

应用场景

  • 临时缓存(如保存线程上下文)
  • 监听器列表(防止内存泄漏)
  • 任何"用完就丢"的辅助数据

面试陷阱

csharp 复制代码
WeakReference<String> ref = new WeakReference<>("Hello");
System.gc();
System.out.println(ref.get()); // 可能还会输出"Hello"!

为什么?因为字符串常量池中的对象有特殊待遇!

5.4. 虚引用(Phantom Reference)------"神出鬼没型"

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

特点

  • 最弱的引用,get()永远返回null
  • 必须配合ReferenceQueue使用
  • 对象被回收时收到通知(就像临终关怀)

资源清理示例

csharp 复制代码
public class ResourceCleaner {
    private static final ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private static final List<PhantomReference<Object>> refs = new ArrayList<>();
    
    public static void track(Object resource, Runnable cleanup) {
        refs.add(new PhantomReference<>(resource, queue) {
            @Override
            public void clear() {
                cleanup.run();
                super.clear();
            }
        });
    }
    
    static {
        // 启动清理线程
        new Thread(() -> {
            while (true) {
                try {
                    Reference<?> ref = queue.remove();
                    ref.clear(); // 触发清理
                } catch (InterruptedException e) { /* 处理中断 */ }
            }
        }).start();
    }
}

// 使用示例
try (InputStream is = new FileInputStream("big.file")) {
    ResourceCleaner.track(is, () -> System.out.println("资源被回收了!"));
    // 使用流...
}

应用场景

  • 精确控制直接内存释放(如NIO的ByteBuffer)
  • 跟踪对象回收时机
  • 实现比finalize()更可靠的清理机制

重要区别

  • finalize():对象第一次GC时调用,可能复活对象
  • 虚引用:对象被完全回收后通知,无法复活

5.5、引用队列(ReferenceQueue)------"死亡通知系统"

所有引用类型都可以关联引用队列:

javascript 复制代码
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> ref = new WeakReference<>(new Object(), queue);

// 当对象被回收后,ref会被加入queue

典型工作流程

  1. 创建引用并关联队列
  2. 当引用对象被回收,引用对象本身会被加入队列
  3. 其他线程从队列获取这些引用做后续处理

5.6、实战中的"组合忍术"

案例1:多级缓存系统

csharp 复制代码
public class TieredCache<K,V> {
    private final Map<K,V> strongCache = new LinkedHashMap<>(); // 一级缓存(强引用)
    private final Map<K,SoftReference<V>> softCache = new HashMap<>(); // 二级缓存
    
    public void put(K key, V value) {
        strongCache.put(key, value);
        softCache.put(key, new SoftReference<>(value));
    }
    
    public V get(K key) {
        V result = strongCache.get(key);
        if (result != null) return result;
        
        SoftReference<V> ref = softCache.get(key);
        if (ref != null) {
            result = ref.get();
            if (result != null) {
                // 重新放入强引用缓存(提升热度)
                strongCache.put(key, result);
                return result;
            }
        }
        return null;
    }
}

案例2:防止内存泄漏的监听器模式

csharp 复制代码
public class EventBus {
    private final Map<EventListener, WeakReference<EventListener>> listeners = 
        new WeakHashMap<>();
    
    public void register(EventListener listener) {
        listeners.put(listener, new WeakReference<>(listener));
    }
    
    public void fireEvent(Event event) {
        listeners.forEach((k,v) -> {
            EventListener listener = v.get();
            if (listener != null) listener.onEvent(event);
        });
    }
}

5.7、引用类型与GC的"爱恨情仇"

不同GC情况下的表现

GC类型 软引用 弱引用 虚引用
Minor GC 可能保留 一定回收 可能回收
Full GC 内存不足时回收 一定回收 可能回收
System.gc() 视内存情况 一定回收 可能回收

性能影响

  • 强引用:无额外开销
  • 软/弱引用:轻微性能影响(需要维护引用队列)
  • 虚引用:较大开销(需要单独线程处理队列)

5.8、灵魂拷问:为什么要有这么多引用类型?

想象你在管理一个图书馆:

  • 强引用:必须归还的书(核心藏书)
  • 软引用:热门小说(没别人预约时可以留着)
  • 弱引用:过期杂志(随时可以清理)
  • 虚引用:书籍销毁记录(知道什么时候书被粉碎了)

5.9、新型"忍者":Cleaner(JDK9+)

java 复制代码
import java.lang.ref.Cleaner;

public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();
    
    private final State state;
    private final Cleaner.Cleanable cleanable;
    
    public Room() {
        this.state = new State();
        this.cleanable = cleaner.register(this, state);
    }
    
    @Override
    public void close() {
        cleanable.clean();
    }
    
    private static class State implements Runnable {
        @Override
        public void run() {
            System.out.println("清理房间资源...");
        }
    }
}

优势

  • 比虚引用更友好API
  • 替代危险的finalize()
  • 适合管理本地内存

第六幕:跨界思考------当GC遇到其他技术

面试官:GC策略会影响Redis这样的内存数据库吗?

小明(眼睛一亮):好问题!虽然Redis自己管理内存,但JVM版的Redis(如Redisson)就会受影响。有趣的是,Redis的淘汰策略和GC有异曲同工之妙:

  • volatile-lru:类似可达性分析,回收最近最少使用的
  • allkeys-lru:全局LRU,像Full GC

面试官:Docker容器中运行Java需要注意什么?

小明:这里有大坑!JVM默认按物理机内存计算堆大小,但在容器中应该:

ruby 复制代码
# 使用容器感知的JVM版本(JDK8u191+)
java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -jar app.jar

否则可能因为超额使用内存被OOM Killer干掉!

终幕:灵魂拷问

面试官:最后,你觉得未来垃圾回收会怎么发展?

小明(沉思状):我认为有几个方向:

  1. 无感GC:像ZGC这样追求亚毫秒停顿
  2. AI预测:通过机器学习预测对象生命周期
  3. 硬件协同:比如Intel的Optane持久内存可能改变GC范式
  4. 语言革新:像Rust的所有权机制或许能启发新的GC思路

面试官(满意地点头):很好,你被录取了!下周一来上班,记得带上你的"垃圾清理"工具包!


怎么样,这场关于"垃圾"的面试是否让你收获满满?记住,优秀的Java程序员不仅要会制造"垃圾",更要懂得高效管理它们。现在,是时候去调优你的JVM参数了------毕竟,谁不想让自己的程序跑得像吃了德芙一样丝滑呢?

(观众掌声雷动,幕布缓缓落下...)

相关推荐
夕颜11110 分钟前
记录一下关于 Cursor 设置的问题
后端
凉白开33811 分钟前
Scala基础知识
开发语言·后端·scala
2401_8242568614 分钟前
Scala的函数式编程
开发语言·后端·scala
yuanbenshidiaos32 分钟前
面试问题总结:qt工程师/c++工程师
c++·qt·面试
uhakadotcom41 分钟前
Langflow:打造AI应用的强大工具
前端·面试·github
uhakadotcom1 小时前
🤖 LangGraph 多智能体群集
面试·架构·github
小杨4041 小时前
springboot框架项目实践应用十四(扩展sentinel错误提示)
spring boot·后端·spring cloud
陈大爷(有低保)1 小时前
Spring中都用到了哪些设计模式
java·后端·spring
程序员 小柴1 小时前
SpringCloud概述
后端·spring·spring cloud
uhakadotcom1 小时前
Caddy Web服务器初体验:简洁高效的现代选择
前端·面试·github