什么是垃圾?如何判断对象已死?

引言:垃圾回收的第一步 当你创建对象后不再使用它们时: public void createObjects() { User user1 = new User("张三", 25); User user2 = new User("李四", 30); // 方法结束后,user1和user2还能被访问吗? } 这些对象会发生什么?它们如何被回收?在回收之前,JVM如何判断它们是否还有用? 这些问题的答案,都从理解 “什么是垃圾” 开始。 垃圾回收的核心问题: 哪些内存需要回收?(什么是垃圾) 什么时候回收?(GC触发时机) 如何回收?(GC算法和收集器) 本文将深入第一个问题:如何判断对象已死。 什么是垃圾? 定义 垃圾(Garbage) = 不再被使用的对象 更准确地说:垃圾是指无法再被程序访问到的对象。 为什么需要垃圾回收? 内存资源是有限的: 堆内存大小有限(如2GB、4GB) 不断创建对象,内存终将耗尽 需要回收不再使用的对象,释放内存 手动管理的问题(如C/C++): 忘记释放内存 → 内存泄漏 释放后继续使用 → 悬空指针 重复释放 → 程序崩溃 Java的自动垃圾回收: 程序员无需手动释放内存 JVM自动识别垃圾并回收 大幅减少内存管理错误 判断对象是否存活的两种算法 判断对象是否是垃圾,有两种主流算法: 引用计数算法(Reference Counting) 可达性分析算法(Reachability Analysis) 方法1:引用计数算法 核心思想 为每个对象添加一个引用计数器: 有引用指向对象时,计数器+1 引用失效时,计数器-1 计数器为0时,对象可被回收 示例 public class ReferenceCountingDemo { public static void main(String[] args) { Object obj = new Object(); // 计数器 = 1 Object obj2 = obj; // 计数器 = 2 obj = null; // 计数器 = 1 obj2 = null; // 计数器 = 0,可被回收 } } 引用计数变化: ...

2025-11-20 · maneng

引用计数法 vs 可达性分析:两种判断算法对比

引言:两种流派的对决 判断对象是否存活,是垃圾回收的第一步。业界有两种主流算法: 引用计数法(Reference Counting)- 简单直接,实时性好 可达性分析法(Reachability Analysis)- 复杂但准确,能解决循环引用 Python选择了引用计数,Java选择了可达性分析。为什么?各有什么优缺点? 本文将深入对比这两种算法,理解它们的工作原理、适用场景,以及主流虚拟机的选择。 引用计数算法详解 核心原理 为每个对象添加一个引用计数器: 初始值为0 每增加一个引用,计数器+1 每减少一个引用,计数器-1 计数器为0时,对象可被回收 详细工作流程 public class ReferenceCountingExample { public static void main(String[] args) { Object obj = new Object(); // ① Object ref1 = obj; // ② Object ref2 = obj; // ③ ref1 = null; // ④ ref2 = null; // ⑤ } } 引用计数变化: 步骤 操作 计数值 说明 ① new Object() 0 → 1 创建对象,obj引用它 ② ref1 = obj 1 → 2 增加引用ref1 ③ ref2 = obj 2 → 3 增加引用ref2 ④ ref1 = null 3 → 2 减少引用ref1 ⑤ ref2 = null 2 → 1 减少引用ref2 方法结束 obj失效 1 → 0 对象可被回收 优点 1️⃣ 实现简单 只需为每个对象添加一个整数计数器,逻辑清晰易懂。 ...

2025-11-20 · maneng

四种引用类型:强、软、弱、虚引用详解

引言:引用的强度 在JDK 1.2之前,Java的引用只有一种:要么有引用,对象存活;要么无引用,对象死亡。 但在实际应用中,我们需要更灵活的引用语义: 缓存对象:内存充足时保留,不足时清理 监控对象:对象被回收时得到通知 弱关联对象:不影响对象的生命周期 JDK 1.2引入了四种引用类型,将引用分为 强引用、软引用、弱引用、虚引用,它们的强度依次递减。 引用强度排序: 强引用 > 软引用 > 弱引用 > 虚引用 理解这四种引用类型,是实现高效缓存、监控对象生命周期的基础。 四种引用类型概览 引用类型 类 回收时机 典型使用场景 强引用 直接引用 永不回收(除非无引用) 普通对象引用 软引用 SoftReference 内存不足时回收 缓存、图片缓存 弱引用 WeakReference GC时必定回收 ThreadLocal、WeakHashMap 虚引用 PhantomReference 无法通过引用获取对象 对象回收监控、堆外内存清理 强引用(Strong Reference) 定义 强引用 是最常见的引用类型,使用 = 赋值运算符创建。 Object obj = new Object(); // 强引用 特点 只要有强引用,对象永不回收 宁可OOM也不回收强引用对象 最普遍的引用方式 示例:强引用不会被回收 public class StrongReferenceDemo { public static void main(String[] args) { Object obj = new Object(); System.out.println("Before GC: " + obj); // 建议GC System.gc(); // 对象仍然存活 System.out.println("After GC: " + obj); // 仍然有效 } } 输出: ...

2025-11-20 · maneng

垃圾回收算法(上):标记-清除、标记-复制

引言:如何回收垃圾? 前面我们学习了如何 判断对象是否是垃圾(可达性分析),现在进入下一个问题:如何回收垃圾? 垃圾回收算法解决的核心问题: 如何标记垃圾对象? 如何清理垃圾对象占用的内存? 如何避免内存碎片? 如何提高回收效率? 业界主流的垃圾回收算法有四种: 标记-清除(Mark-Sweep) 标记-复制(Mark-Copy) 标记-整理(Mark-Compact) 分代收集(Generational Collection) 本文将深入学习前两种基础算法:标记-清除和标记-复制。 标记-清除算法(Mark-Sweep) 核心思想 分两个阶段: 标记阶段(Mark):标记所有存活的对象 清除阶段(Sweep):清除所有未标记的对象 详细流程 阶段1:标记(Mark) 从GC Roots开始,标记所有可达对象。 GC Roots开始遍历 ↓ 标记对象A(可达) ↓ 标记对象B(A引用) ↓ 标记对象C(B引用) ↓ ... 完成标记 阶段2:清除(Sweep) 遍历堆,清除所有未标记的对象。 遍历堆内存 ↓ 遇到未标记对象 → 释放内存 遇到已标记对象 → 保留(清除标记) ↓ 完成清除 图解示例 初始状态: 堆内存: ┌────┬────┬────┬────┬────┬────┬────┬────┐ │ A │ B │ C │ D │ E │ F │ G │ H │ └────┴────┴────┴────┴────┴────┴────┴────┘ GC Roots引用: A, C, E 标记阶段: ...

2025-11-20 · maneng

垃圾回收算法(下):标记-整理、分代收集

引言:完善的垃圾回收方案 上一篇我们学习了两种基础算法: 标记-清除:简单但有碎片 标记-复制:无碎片但浪费空间 本文将学习另外两种算法,它们解决了前两种算法的缺陷: 标记-整理:无碎片且不浪费空间 分代收集:综合运用多种算法,是现代JVM的主流方案 标记-整理算法(Mark-Compact) 核心思想 分三个阶段: 标记阶段(Mark):标记所有存活对象 整理阶段(Compact):将存活对象移动到内存一端 清除阶段(Sweep):清除边界外的所有内存 与标记-清除的区别 算法 清除方式 内存布局 标记-清除 直接释放死亡对象 分散,有碎片 标记-整理 移动存活对象后清除 紧凑,无碎片 详细流程 阶段1:标记(Mark) 从GC Roots开始,标记所有可达对象。 GC Roots开始遍历 ↓ 标记对象A(可达) ↓ 标记对象B(A引用) ↓ 标记对象C(B引用) ↓ 完成标记 阶段2:整理(Compact) 将所有存活对象移动到内存的一端,使其紧密排列。 整理前: ┌────┬────┬────┬────┬────┬────┬────┬────┐ │ A │ 空 │ C │ 空 │ E │ 空 │ 空 │ H │ └────┴────┴────┴────┴────┴────┴────┴────┘ 整理后: ┌────┬────┬────┬────┬────────────────────┐ │ A │ C │ E │ H │ 空 │ └────┴────┴────┴────┴────────────────────┘ 阶段3:清除(Sweep) 清除边界后的所有内存。 ...

2025-11-20 · maneng

垃圾收集器演进史:从Serial到ZGC

垃圾收集器发展历程 时间线 年份 收集器 特点 适用场景 1999 Serial 单线程 客户端 2002 Parallel 并行,高吞吐量 服务端 2004 CMS 并发,低延迟 互联网应用 2012 G1 分区,可预测停顿 大内存应用 2018 ZGC/Shenandoah 超低延迟 大内存、低延迟要求 核心概念 并行(Parallel)vs 并发(Concurrent) 并行(Parallel): 多个GC线程同时工作 仍需Stop The World 缩短停顿时间 应用线程: ████████ [暂停] ████████ GC线程1: ▓▓▓▓▓ GC线程2: ▓▓▓▓▓ GC线程3: ▓▓▓▓▓ 并发(Concurrent): GC线程与应用线程同时运行 减少Stop The World时间 实现更复杂 应用线程: ████████████████████████ GC线程: ░░░░░░░░░░░░░░░░░░░░░░░░ (大部分时间并发执行) 吞吐量 vs 低延迟 吞吐量优先(Throughput): 关注总体执行效率 适合后台计算、批处理 代表:Parallel Scavenge 低延迟优先(Low Latency): 关注单次停顿时间 适合Web应用、交互式应用 代表:CMS、G1、ZGC 权衡: 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间) 例如: · 100秒运行时间,GC耗时5秒 → 吞吐量 = 95% · 低延迟可能牺牲吞吐量 收集器组合 经典组合(JDK 8) 新生代收集器 老年代收集器 ┌─────────────┐ ┌──────────────┐ │ Serial │────────▶│ Serial Old │ └─────────────┘ └──────────────┘ ↓ ┌─────────────┐ ┌──────────────┐ │ ParNew │────────▶│ CMS │ └─────────────┘ └──────────────┘ ↓ ┌─────────────┐ ┌──────────────┐ │ Parallel │────────▶│ Parallel Old │ │ Scavenge │ └──────────────┘ └─────────────┘ ┌────────────────────────────────────┐ │ G1收集器 │ │ (统一收集新生代和老年代) │ └────────────────────────────────────┘ ┌────────────────────────────────────┐ │ ZGC / Shenandoah │ │ (低延迟收集器) │ └────────────────────────────────────┘ 收集器对比 收集器 类型 算法 停顿时间 吞吐量 适用场景 Serial 新生代 复制 长 低 客户端 ParNew 新生代 复制 中 中 配合CMS Parallel Scavenge 新生代 复制 中 高 后台计算 Serial Old 老年代 标记-整理 长 低 客户端 Parallel Old 老年代 标记-整理 中 高 后台计算 CMS 老年代 标记-清除 短 中 Web应用 G1 全堆 标记-整理 + 复制 可控 高 大内存应用 ZGC 全堆 着色指针 极短 高 超大堆 Shenandoah 全堆 转发指针 极短 高 超大堆 选择建议 应用类型与收集器 1. 单CPU、小内存(<100MB) ...

2025-11-20 · maneng

Serial/Serial Old:单线程收集器

核心特点 Serial收集器: 单线程收集器(只用一个线程进行GC) 新生代收集器 使用复制算法 必须暂停所有应用线程(Stop The World) Serial Old收集器: Serial的老年代版本 使用标记-整理算法 工作流程 应用线程执行 ↓ 触发GC ↓ Stop The World(暂停所有应用线程) ↓ 单个GC线程执行垃圾回收 ↓ 恢复应用线程 图解: 应用线程: ████████ [暂停] ████████ GC线程: ▓▓▓▓▓▓ (单线程) 优缺点 优点 简单高效:单线程无需线程切换开销 内存占用小:适合小堆内存 客户端模式默认:适合桌面应用 缺点 Stop The World时间长:单线程收集效率低 不适合服务端:停顿时间无法接受 使用方法 # 启用Serial收集器(新生代)+ Serial Old(老年代) java -XX:+UseSerialGC -jar app.jar # 查看GC日志 java -XX:+PrintGCDetails -XX:+UseSerialGC -jar app.jar 适用场景 客户端应用:桌面应用、小工具 小内存应用:堆内存<100MB 单CPU环境:无法利用多线程优势 GC日志示例 [GC (Allocation Failure) [DefNew: 3072K->320K(3456K), 0.0015428 secs] 3072K->1344K(11904K), 0.0015779 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 解读: · DefNew:新生代使用Serial收集器 · 3072K->320K:新生代从3MB降到320KB · (3456K):新生代总大小3.5MB · 0.0015428 secs:GC耗时1.5毫秒 总结 Serial:最古老、最简单的收集器 单线程:Stop The World时间长 适用场景:客户端、小内存应用 现代应用:基本不使用(除非资源极度受限) 下一篇预告:《ParNew/Parallel:并行收集器》 ...

2025-11-20 · maneng

ParNew/Parallel:并行收集器

ParNew收集器 核心特点 Serial的多线程版本 新生代收集器 使用复制算法 可与CMS配合使用 工作流程 应用线程: ████████ [暂停] ████████ GC线程1: ▓▓▓▓ GC线程2: ▓▓▓▓ GC线程3: ▓▓▓▓ GC线程4: ▓▓▓▓ 参数配置 # 启用ParNew(新生代)+ CMS(老年代) java -XX:+UseConcMarkSweepGC -jar app.jar # (ParNew会自动启用) # 设置并行GC线程数 java -XX:ParallelGCThreads=4 -jar app.jar Parallel Scavenge收集器 核心特点 吞吐量优先收集器 新生代收集器 使用复制算法 自适应调节策略(GC Ergonomics) 与ParNew的区别 特性 ParNew Parallel Scavenge 目标 配合CMS降低延迟 最大化吞吐量 自适应 无 有 配合老年代 CMS Parallel Old 关键参数 # 启用Parallel Scavenge(新生代)+ Parallel Old(老年代) java -XX:+UseParallelGC -jar app.jar # 设置吞吐量目标(默认99,即GC时间占1%) java -XX:GCTimeRatio=19 -jar app.jar # 吞吐量 = 1 / (1 + 1/GCTimeRatio) # GCTimeRatio=19 → 吞吐量=95% # 设置最大GC停顿时间(毫秒) java -XX:MaxGCPauseMillis=200 -jar app.jar # 启用自适应调节(默认开启) java -XX:+UseAdaptiveSizePolicy -jar app.jar Parallel Old收集器 核心特点 Parallel Scavenge的老年代版本 使用标记-整理算法 多线程并行收集 组合使用 # 完整的并行收集器组合(JDK 7/8常用) java -XX:+UseParallelGC -XX:+UseParallelOldGC \ -XX:MaxGCPauseMillis=100 \ -XX:GCTimeRatio=19 \ -jar app.jar 自适应调节策略 Parallel Scavenge的核心优势是 自适应调节: ...

2025-11-20 · maneng

CMS收集器:并发标记清除详解

核心特点 CMS(Concurrent Mark Sweep) - 并发标记清除: 目标:最短停顿时间 老年代收集器 并发执行:大部分时间与应用线程并发 算法:标记-清除 四个阶段 1️⃣ 初始标记(Initial Mark)【STW】 工作:标记GC Roots直接引用的对象 特点:Stop The World,但速度很快 应用线程: ████████ [暂停] ████████████████ GC线程: ▓ (极短) 2️⃣ 并发标记(Concurrent Mark) 工作:从GC Roots开始遍历整个对象图 特点:与应用线程并发执行,耗时最长 应用线程: ████████████████████████████ GC线程: ░░░░░░░░░░░░░░░░░░░░░░░░ (并发执行) 3️⃣ 重新标记(Remark)【STW】 工作:修正并发标记期间变化的对象 特点:Stop The World,时间略长于初始标记 应用线程: ████████████ [暂停] ████████ GC线程: ▓▓▓ 4️⃣ 并发清除(Concurrent Sweep) 工作:清除标记为死亡的对象 特点:与应用线程并发执行 应用线程: ████████████████████████████ GC线程: ░░░░░░░░░░░░░░░░░░░░░░░░ 完整流程图 ┌──────────────┐ │ 初始标记(STW) │ ← 极短停顿 └──────┬───────┘ ↓ ┌──────────────┐ │ 并发标记 │ ← 耗时最长,并发 └──────┬───────┘ ↓ ┌──────────────┐ │ 重新标记(STW) │ ← 短停顿 └──────┬───────┘ ↓ ┌──────────────┐ │ 并发清除 │ ← 并发 └──────────────┘ 优点 低停顿:大部分时间并发执行 适合互联网应用:对响应时间敏感的场景 缺点 1️⃣ CPU资源敏感 并发阶段占用CPU资源,影响应用吞吐量。 ...

2025-11-20 · maneng

G1收集器:划时代的垃圾收集器

核心特点 G1(Garbage-First) 收集器: 面向服务端的收集器(JDK 9默认) 分区设计:将堆划分为多个Region 可预测停顿:设置目标停顿时间 混合收集:同时收集新生代和老年代 整体标记-整理,局部复制:避免碎片 Region设计 传统堆 vs G1堆 传统堆: ┌────────────────────┐ │ 新生代 (固定) │ ├────────────────────┤ │ 老年代 (固定) │ └────────────────────┘ G1堆: ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ E │ E │ S │ O │ O │ H │ O │ E │ ├───┼───┼───┼───┼───┼───┼───┼───┤ │ O │ E │ S │ O │ E │ O │ E │ O │ └───┴───┴───┴───┴───┴───┴───┴───┘ E - Eden区 S - Survivor区 O - Old区 H - Humongous区(大对象) Region特点 每个Region大小相同(1MB-32MB,2的幂) 动态分配角色(Eden/Survivor/Old) 大对象(>= Region一半)占用连续Region G1的GC类型 1️⃣ Young GC 触发:Eden区满 ...

2025-11-20 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...