目录
二、“垃圾” 指不再被任何存活对象引用的对象。判断对象是否存活的核心算法有两种:
2. 可达性分析算法(Reachability Analysis)
三、Java 中的 四种“引用” 类型(影响存活判断)Java 通过java.lang.ref包定义了 4 种引用类型,优先级从高到低:
特点:单线程执行 GC,回收时暂停所有应用线程(STW,Stop-The-World)。
优点:实现简单,内存占用少,适合单 CPU、小堆场景(如嵌入式设备)。
缺点:STW 时间长(堆越大,停顿越久),不适合高并发应用。
3. CMS(Concurrent Mark Sweep,并发标记清除,停顿时间短)
垃圾回收(Garbage Collection,简称 GC)是编程语言中自动管理内存的核心机制,其核心目标是识别并回收 “不再被使用的对象” 所占用的内存,避免内存泄漏,确保程序高效、安全地运行。以下从核心目标、垃圾判断标准、基本流程、经典算法、常见回收器及关键概念等方面详细总结:
一、垃圾回收的核心目标
自动释放无用内存:无需开发者手动调用free()或delete,自动识别 “垃圾对象” 并回收其内存。
减少内存泄漏:通过精准识别无用对象,避免长期占用内存导致的程序性能下降或崩溃。
平衡效率与安全性:在保证内存回收准确性的同时,尽可能减少对程序运行的干扰(如缩短停顿时间)。
二、“垃圾” 指不再被任何存活对象引用的对象。判断对象是否存活的核心算法有两种:
1. 引用计数法(Reference Counting)
原理:为每个对象维护一个 “引用计数器”,当对象被引用时计数器 + 1,引用失效时 - 1;当计数器为 0 时,判定为垃圾。
优点:实现简单,回收及时(计数器为 0 时立即回收)。
缺点:
无法解决循环引用问题(如 A 引用 B,B 引用 A,两者计数器均不为 0,但实际已无用)。
频繁更新计数器会增加性能开销。
2. 可达性分析算法(Reachability Analysis)
核心思想:以 “根对象(GCRoots)” 为起点,遍历所有可直接或间接引用的对象(形成 “引用链”);无法被根对象触达的对象判定为垃圾。
根对象(GCRoots)类型:
1.虚拟机栈中正在使用的局部变量(如方法参数、临时变量)。
2.方法区中静态变量(static修饰)和常量(final修饰)。
3.本地方法栈中 JNI(Native 方法)引用的对象。
4.虚拟机内部的引用(如类加载器、基本数据类型对应的 Class 对象)。
优点:解决循环引用问题,是 Java、C# 等语言的核心判断方式。
三、Java 中的 四种“引用” 类型(影响存活判断)
Java 通过java.lang.ref包定义了 4 种引用类型,优先级从高到低:
强引用:默认引用(如Object obj = new Object()),只要存在,对象永远不被回收。
软引用(SoftReference):内存不足时(即将 OOM)才会被回收,适合缓存场景(如图片缓存)。
弱引用(WeakReference):只要发生 GC 就会被回收,适合临时数据(如 ThreadLocal 的 key)。
虚引用(PhantomReference):仅用于跟踪对象回收过程,必须配合引用队列使用,无实际引用意义。
四、经典垃圾回收算法
1.标记 - 清除算法
- 标记:通过可达性分析标记所有存活对象。
- 清除:回收未标记的垃圾对象,保留内存地址空闲。
- 优点:实现简单,无需移动对象,适用于存活对象多的场景。
- 缺点:
- 1.产生内存碎片(回收后空闲内存分散),可能导致大对象无法找到足够的连续空间可以分配。
- 2.清除过程效率低(需遍历全堆找垃圾)。
2.复制算法
- 将内存划分为大小相等的两块(如 From 区和 To 区),仅使用其中一块(From 区)。
- 标记:标记 From 区中的存活对象。
- 复制:将存活对象复制到 To 区,按顺序紧凑排列。
- 切换:清空 From 区,下次 GC 时 From 区和 To 区角色互换。
- 优点:无内存碎片,分配内存时只需移动指针(类似 “指针碰撞”),效率高。
- 缺点:
- 1.内存利用率低(仅用一半)。
- 2.复制成本高(若存活对象多,复制耗时)。
- 适用场景:存活对象少的区域(如 Java 新生代的 Eden 区与 Survivor 区)。
3.标记 - 整理算法
- 标记:标记所有存活对象。
- 整理:将存活对象向内存一端 “紧凑排列”,消除碎片。
- 清除:回收末端的垃圾对象(因存活对象已集中,末端空闲内存可直接释放)。
- 优点:无内存碎片,内存利用率 100%。
- 缺点:整理阶段需要移动对象,成本高(尤其存活对象多时)。
- 适用场景:存活对象多的区域(如 Java 老年代)。
4.分代回收算法(Generational Collection)
- 核心依据:对象存活周期不同(“朝生夕死” 的对象占多数,少数对象长期存活)。
- 内存划分:将堆分为新生代(Young Generation)和老年代(Old Generation):
- 新生代:存放新创建的对象,存活时间短(多数一次 GC 就被回收)。
- 老年代:存放存活时间长的对象(多次 GC 后仍存活)。
- 回收策略:
- 新生代:采用标记 - 复制算法(因存活对象少,复制成本低)。
- 细分为 Eden 区(80%)和两个 Survivor 区(S0、S1 各 10%),每次使用 Eden+S0,回收时复制存活对象到 S1,清空 Eden+S0,下次 S0 与 S1 互换。
- 老年代:采用标记 - 整理或标记 - 清除算法(因存活对象多,移动成本高)。
- 优势:按代针对性回收,减少全堆扫描,提高效率。
五、常见垃圾回收器(具体实现)
垃圾回收器是 GC 算法的具体实现,不同回收器侧重不同(吞吐量、延迟等),以下是 Java 中主流回收器:
1. Serial GC(串行回收器)
-
特点:单线程执行 GC,回收时暂停所有应用线程(STW,Stop-The-World)。
-
算法:新生代用标记 - 复制,老年代用标记 - 整理。
-
优点:实现简单,内存占用少,适合单 CPU、小堆场景(如嵌入式设备)。
-
缺点:STW 时间长(堆越大,停顿越久),不适合高并发应用。
2. Parallel GC(并行回收器)
- 特点:多线程执行 GC,仍有 STW,但比 Serial GC 快(利用多核优势),侧重吞吐量(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + GC 时间))。
- 算法:新生代用标记 - 复制,老年代用标记 - 整理。
- 优点:吞吐量高,适合后台任务、批处理等对延迟不敏感的场景。
- 缺点:STW 时间仍较长,无法满足低延迟需求。
3. CMS(Concurrent Mark Sweep,并发标记清除,停顿时间短)
- 特点:以低延迟为目标,尽可能减少 STW 时间,核心阶段与应用线程并发执行。
- 流程(基于三色标记算法):
- 初始标记(STW):标记根对象直接引用的对象(快)。
- 并发标记:与应用线程并行,遍历根对象可达的所有对象(无 STW)。
- 重新标记(STW):修正并发标记中因应用线程修改引用导致的漏标(快)。
- 并发清除:与应用线程并行,回收垃圾对象(无 STW)。
- 优点:STW 时间短,适合对延迟敏感的应用(如 Web 服务)。
- 缺点:
- 并发阶段占用 CPU 资源,降低程序吞吐量。
- 产生内存碎片(基于标记 - 清除),需定期 Full GC 整理。
- 无法处理 “浮动垃圾”(并发清除时新产生的垃圾,需下次 GC 回收)。
4. G1(局域性)混合模式
- 特点:区域化分代式回收器,兼顾吞吐量和延迟,支持大堆(可达数 TB)。
- 内存划分:将堆分为多个大小相等的 Region(区域),每个 Region 可动态标记为新生代(Eden/Survivor)或老年代。
- 核心思想:优先回收 “垃圾多的 Region”(Garbage-First),减少回收时间。
- 流程(基于三色标记 + SATB 原始快照):
- 初始标记(STW):标记根对象直接引用。
- 并发标记:遍历可达对象,计算每个 Region 的垃圾占比。
- 最终标记(STW):处理并发阶段漏标对象。
- 筛选回收(STW):选择垃圾占比高的 Region,复制存活对象到新 Region(同时整理碎片)。
- 优点:可预测 STW 时间(通过参数设置最大停顿时间),无碎片,适合大堆、中低延迟场景。
混合模式:G1 采用局部性收集的思想,对于堆空间的划分,采用 Region 为单位的内存划分方式:
G1 垃圾回收器把堆划分成若干个大小相同的独立区域(Region)(按照 JVM 的实现,Region 的数量不会超过 2048 个):每个 Region 都会代表Eden 区,Survivor 区, Humongous(G1 用来分配大对象的区域,对于 Humongous 也分配不下的超大对象,会分配在连续的 N 个 Humongous 中),剩余的深蓝色代表的是 Old 区,灰色的代表的是空闲的 region。
这种思想上的转变和设计,使得 G1 可以面向堆内存任何部分来组成回收集来进行回收,衡量标准不再是它属于哪个分代,而是哪块内存存放的垃圾最多,回收收益最大,这就是 G1 收集器的 Mixed GC 模式,即混合 GC 模式。
垃圾收集器 | 过程 | 算法 | 缺点 | 优点 |
Serial(新生代) Serial Old(老年代) | 标记-复制 标记-整理 | 标记-复制 标记-整理 | 1.内存利用率低(仅用一半)。 | 实现简单,内存占用少,适合单 CPU、小堆场景(如嵌入式设备) |
ParNew(新生代) | (多线程)标记-复制 | 标记-复制 | 同上 | 同上 |
Parallel Scavenge(新生代) Parallel Old(老年代) | (多线程)标记-复制 (多线程)标记-整理 | 标记-复制 标记-整理 | 不能保证单次的停顿时间 | 只关注吞吐量(可以手动调整参数),适用于后台任务 |
CMS(老年代) | 1. 初始标记,用极短的时间标记出 GC Roots 能直接关联到的对象。 2. 并发标记,标记所有的对象,用户线程不需要暂停。 3. 重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。 4. 并发清理,清理死亡的对象,用户线程不需要暂停。 | 标记-清除 | 1.内存碎片(清除操作不会移动对象来填补空间) 2.在并发清除阶段,用户线程还没有停止,还会产生新的垃圾,这就是“浮动垃圾” 3.在并发标记和并发清除时会和用户线程抢占资源 | 1.STW时间最短 2.多线程 |
G1(局域性收集) |
| 标记,整理加复制 | 1.不适合小场景下使用 | 1.可以控制垃圾的回收的停顿时间 2.不会产生内存碎片 |
六、三色标记
白色:1.默认 2.垃圾 3.未被标记
灰色:自身已被标记到,但引用的子对象还未被标记
黑色:自身以及所引用的所有子对象都已被标记