JVM垃圾收集器相关面试题(1)
垃圾收集与内存管理摘要
一.核心垃圾收集算法对比
算法 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
标记-清除 | 两次遍历(标记存活对象→清除未标记对象) | 实现简单 | 内存碎片化、双遍历效率低 | 老年代(结合整理) |
标记-复制 | 内存对半分,存活对象复制到空白区 | 无碎片、效率高 | 内存利用率50% | 新生代 |
标记-整理 | 标记后移动存活对象至内存端 | 无碎片、内存利用率高 | 对象移动开销大 | 老年代 |
二.分代收集核心机制
内存分代结构
-
新生代(1/3堆)
- Eden区:80%空间,新对象初始分配区,通过连续内存分配优化短命对象处理
- Survivor区:From/To各10%,存放至少存活一次的对象,采用复制算法
-
老年代(2/3堆):存放长周期对象,采用标记-清除/整理算法
-
元空间(本地内存):类元数据存储,GC条件更宽松
回收流程关键点
-
Minor GC(新生代)
- 触发条件:Eden区满
- 对象晋升:年龄阈值(15)或Survivor空间不足
- 复制效率:仅处理存活对象,存活率<10%时最优
-
Full GC(全局)
- 触发条件:老年代不足/显式调用
- 性能影响:全堆扫描,停顿时间显著
三.永久代演进对比
特性 | Java7永久代 | Java8+元空间 |
---|---|---|
存储位置 | 堆内存 | 本地内存 |
GC触发条件 | 类+类加载器+反射引用全解除 | 类加载器回收即释放 |
内存管理 | 固定大小易OOM | 动态扩展 |
性能影响 | Full GC时扫描 | 独立回收机制 |
四.对象存活判定
- 可达性分析法:通过GC Roots(栈变量、静态属性、JNI引用)遍历引用链
- 死亡判定流程:两次标记机制(可达性分析→finalize()自救机会→不可达回收)
关键设计理念:基于对象生命周期特征(98%对象朝生夕死)进行分代优化,通过空间换时间(复制算法)和延迟处理(老年代整理)平衡吞吐量与停顿时间。
正文
一.常见的垃圾收集算法
标记-清楚
原理:
-
标记:从GC Roots(例如栈中的变量,静态变量等)开始便利,标记出所有被引用的对象
-
清除:遍历整个堆,清除没被标记的对象。
缺点
- 标记和清除各遍历一次,效率低下
- 可能产生大量不连续的空间,当程序需要分配较大内存时,可能会因为无法找到连续的内存空间导致内存不足,从而提前触发垃圾回收。
标记-复制
原理:将内存空间化为等大的两块,一般称为(Form和To空间)。每次只是用其中的一块,当这一块用满后,就将存活的对象复制到另一块空间,然后把原来使用的空间直接清理掉。
优点:
- 只需要复制存活的对象效率很高
- 清除后的空间是连续的
缺点
- 内存空间只占用一般,另一半用来存放复制后的内存,空间浪费严重
标记-整理
原理:
- 标记:与标记-清除算法相同,从GC Root开始遍历所有存活的对象
- 整理:将存活对象向内存的一端移动,此过程不会清除垃圾对象,而是会把存活对象直接挪到垃圾对象,类似于赋值操作,然后直接清理掉边界以外的内存空间。
优点
- 解决了标记-清除算法需要遍历两次和会产生内存碎片的问题,同时也不会和标记-整理算法一样浪费一半的空间
缺点
- 整理过程需要移动对象,效率相对较低,尤其是对象过多的情况下
分代收集
原理
-
基于对象存活周期不同,讲内存化为不同的代
-
新生代:对象通常“朝生夕死”存活率低。一般采用标记-复制算法
-
老生代:对象通常存活率高,占用空间大。一般采用标记-清除或标记-整理算法
优点
- 根据对象存活特点采用不同算法,提高垃圾回收效率,减少对应用程序性能的影响。
缺点
- 需要对堆内存进行分代管理,增加了垃圾回收器的实现复杂度。
二.分代垃圾回收器工作原理详解
分代垃圾回收器是基于一个假说:大部分对象生命周期极短,少数对象长期存活。
堆内存分代结构
-
新生代
-
占堆内存的1/3
-
分为Eden区(80%)和两个Survivor区(From+To 各10%)
- 为什么要有Eden区?直接和正常的标记-复制算法一样不好吗?只要From和To两个分区
- 因为大多数内存活不过一次GC,新对象直接在连续内存的 Eden 区分配,避免频繁内存整理。集中处理 “朝生暮死” 的对象,减少对 Survivor 区的频繁操作。
如果只是用From和To两个Survivor区域,则无法隔离新对象和多次存活对象,导致每次 GC 需扫描全部区域,效率降低。
所以我们再Survivor中存放的是至少存货过一次的对象,Eden区只存放新对象
-
新对象优先在Eden区分配,若Eden区空间不足,则出发Minor GC
-
-
老生代
- 占堆内存2/3,存放长期存活的对象
- 当老生代空间不足时,则出发Full GC或Major GC,回收整个堆
-
元空间
- 取代永久代,存放类元数据,常量池等,GC主要针对不在使用的类的加载器和常量池
对象分配与回收流程
1.对象分配
-
新对象先分配到Eden区,若Eden区已满,则出发Minor GC
-
大对象(如长数组)会直接进入老年代
2.Minor GC(新生代回收)
前面我们总体到Minor GC,那么他到底是个什么呢?
- 存活对象:从 Eden 和 Survivor 区复制到另一个 Survivor 区(复制算法)。
- 对象年龄:每熬过一次 Minor GC,年龄 + 1。
- 晋升老年代:年龄达到阈值(默认 15)或 Survivor 区空间不足时晋升
3.Full GC(老年代回收):
-
触发条件:老年代空间不足、显式调用System.gc()等。
-
使用标记 - 清除或标记 - 整理算法,回收整个堆,耗时长
三.JVM 永久代中会发生垃圾回收吗
-
Java8前 永久代
- 存储位置:JVM堆内存
- GC 条件:类需满足:所有实例被回收 + 类加载器被回收 + 无反射引用。
- 问题:容易因类加载过多引发` ```rustOutOfMemoryError: PermGen space````
-
Java8+ 元空间
-
存储位置:本地内存,不再占用堆。
-
GC 条件:仅需类加载器被回收,自动释放类元数据。
-
改进:
- 内存动态扩展,内存取决于物理内存大小
- 减少 OOM 风险,GC 效率更高。
-
四.如何判断对象是否存活?
可达性算法
-
原理: 从CG Roots出发,遍历所有的引用链,无法到达的对象即为死亡
-
GC Roots 包括:
- 虚拟机栈中局部变量引用的对象。
- 方法区中静态变量和常量引用的对象。
- 本地方法栈中 JNI (Java Native Interface)引用的对象