编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 序卷方法论

  • 数据的本质

  • 运行时模型

  • 并发的设计

  • 内存的真相

    • README
    • 1.虚拟内存与地址空间
    • 2.内存模型技术设计
    • 3.堆和栈内存的设计
    • 4.内存对齐与缓存局部性
    • 5.内存回收机制设计
    • 6.多种引用技术设计
    • 7.内存泄漏与诊断原理
      • 00.真实事故引入
        • 0.1 午夜OOM难找
        • 0.2 100MB/天泄漏
        • 0.3 灵魂三问
        • 0.4 五个递进追问
        • 0.5 探索路径
        • 0.6 为何值得讲透
        • 0.7 泄漏五语言对照
      • 01.内存泄漏本质
        • 1.1 GC活与业务活
        • 1.2 泄漏四种范式
        • 1.3 四范式共同本质
        • 1.4 资源vs内存泄漏
      • 02.引用链分析
        • 2.1 GC Roots起点
        • 2.2 Shallow与Retained
        • 2.3 支配树
        • 2.4 GC Root路径
      • 03.Heap Dump原理
        • 3.1 触发时机
        • 3.2 STW 代价
        • 3.3 HPROF 格式
        • 3.4 增量流式dump
      • 04.Profiler原理
        • 4.1 采样与精确
        • 4.2 AllocationTracking
        • 4.3 火焰图设计
        • 4.4 Async Profiler
      • 05.移动端的特殊难题
        • 5.1 LeakCanary革命
        • 5.2 iOS MLeaksFinder
        • 5.3 Valgrind与ASan
      • 06.跨平台诊断工具对照
        • 6.1 工具矩阵
        • 6.2 pprof vs MAT
        • 6.3 Node heap
        • 6.4 Python追踪
        • 6.5 采样vs快照
      • 07.经典陷阱反模式
        • 7.1 监听器忘反注册
        • 7.2 缓存无界增长
        • 7.3 ThreadLocal未清
        • 7.4 单例持Activity
        • 7.5 内部类隐式持有
        • 7.6 监控指标缺失
        • 7.7 生产dump大堆
      • 08.综合案例串讲
        • 8.1 时间线总览
        • 8.2 电商搜索背景
        • 8.3 T+0告警大盘
        • 8.4 T+1抓Heap Dump
        • 8.5 T+2 MAT支配树
        • 8.6 T+3.5锁定真凶
        • 8.7 T+5 Fix提交
        • 8.8 T+12 JFR监控
        • 8.9 知识点回归
        • 8.10 一句话提炼
      • 09.一句话总结
        • 9.1 三层认知阶梯
        • 9.2 诊断流程图
        • 9.3 七字真言
        • 9.4 与下篇承接
      • 🔗 延伸阅读
    • 8.数据拷贝设计原理
  • 交互和系统

  • 内功
  • 内存的真相
杨充
2026-05-14
目录

7.内存泄漏与诊断原理

# 4.7 内存泄漏与诊断原理

📍 本篇位置:第 4 卷 · 内存的真相 · 第 7 篇 🎯 核心矛盾:有了 GC 还会泄漏?是的,而且泄漏比手动管理时代更隐蔽——监听器忘反注册、缓存无界、ThreadLocal 没清理、单例持有 Activity……比泄漏本身更难的是"找出凶手" 🧭 设计灵魂:内存泄漏诊断的全部哲学,是把"看不见"变成"看得见"——Heap Dump 把瞬时内存"快照"成可分析的文件,Reference Chain 把"GC 为什么不回收"的原因可视化,Profiler 把"分配热点"暴露出来。好的诊断工具 = 让看不见的东西看得见 🌐 跨平台覆盖:JVM Heap Dump + MAT · Android LeakCanary · iOS MLeaksFinder · Go pprof · Node.js heap snapshot · Python tracemalloc · C++ Valgrind / ASan 🔗 延伸阅读:← 4.5 内存回收机制设计 (opens new window) · ← 4.6 多种引用技术设计 (opens new window) · → 4.8 数据拷贝设计原理 (opens new window)


4.5/4.6 我们看到了 GC 怎么"自动管理"内存,引用技术怎么控制对象生命周期。但工程现实是残酷的——即便有 GC,内存泄漏依然普遍存在。

比泄漏本身更难的,是如何把泄漏暴露出来。本篇从一次"OOM 救火"的全过程切入,剖开 Profiler、Heap Dump、引用链分析这些工具背后的原理——把"诊断内存"从黑魔法变成可解释的工程方法。

# 目录介绍

  • 00.真实事故引入
    • 0.1 午夜OOM难找
    • 0.2 100MB/天泄漏
    • 0.3 灵魂三问
    • 0.4 五个递进追问
    • 0.5 探索路径
    • 0.6 为何值得讲透
    • 0.7 泄漏五语言对照
  • 01.内存泄漏本质
    • 1.1 GC活与业务活
    • 1.2 泄漏四种范式
    • 1.3 四范式共同本质
    • 1.4 资源vs内存泄漏
  • 02.引用链分析
    • 2.1 GC Roots起点
    • 2.2 Shallow与Retained
    • 2.3 支配树
    • 2.4 GC Root路径
  • 03.Heap Dump原理
    • 3.1 触发时机
    • 3.2 STW 代价
    • 3.3 HPROF 格式
    • 3.4 增量流式dump
  • 04.Profiler原理
    • 4.1 采样与精确
    • 4.2 AllocationTracking
    • 4.3 火焰图设计
    • 4.4 Async Profiler
  • 05.移动端的特殊难题
    • 5.1 LeakCanary革命
    • 5.2 iOS MLeaksFinder
    • 5.3 Valgrind与ASan
  • 06.跨平台诊断工具对照
    • 6.1 工具矩阵
    • 6.2 pprof vs MAT
    • 6.3 Node heap
    • 6.4 Python追踪
    • 6.5 采样vs快照
  • 07.经典陷阱反模式
    • 7.1 监听器忘反注册
    • 7.2 缓存无界增长
    • 7.3 ThreadLocal未清
    • 7.4 单例持Activity
    • 7.5 内部类隐式持有
    • 7.6 监控指标缺失
    • 7.7 生产dump大堆
  • 08.综合案例串讲
    • 8.1 时间线总览
    • 8.2 电商搜索背景
    • 8.3 T+0告警大盘
    • 8.4 T+1抓Heap Dump
    • 8.5 T+2 MAT支配树
    • 8.6 T+3.5锁定真凶
    • 8.7 T+5 Fix提交
    • 8.8 T+12 JFR监控
    • 8.9 知识点回归
    • 8.10 一句话提炼
  • 09.一句话总结
    • 9.1 三层认知阶梯
    • 9.2 诊断流程图
    • 9.3 七字真言
    • 9.4 与下篇承接

# 00.真实事故引入

# 0.1 午夜OOM难找

我曾在一个支付系统当过半夜救火队长。某次告警惊心动魄:

00:00:00  服务运行正常,内存 4.2GB
00:30:00  内存 6.1GB(开始警觉)
01:00:00  内存 7.8GB(接近 -Xmx 8GB)
01:15:00  Full GC 频繁,每分钟 5 次
01:23:00  OutOfMemoryError,服务进程死亡
01:25:00  k8s 重启服务,重新开始循环

每天准点炸——24 小时一次。开发同事的反应:

"GC 应该会回收啊?怎么会泄漏?JVM 不是号称 'no memory leak'?"

我们先做了最直接的——dump 堆:

jmap -dump:live,format=b,file=heap.bin <pid>
# 8GB 的 dump 文件,传到本地分析

用 Eclipse MAT 打开后——一个让人崩溃的画面:

所有对象按 retained size 排序:

第 1 名:byte[]            占用 6.5GB
        ↑ 这是个数组,没用,但占了 80% 的堆!
        
第 2 名:HashMap$Entry[]   占用 800MB
第 3 名:String           占用 400MB
...

新人的反应:直接看名字根本看不出问题——byte[] 是字节数组,到处都在用。哪个 byte[] 是凶手?

这就是新人和老司机的差距——找泄漏的核心不是"看占了多少",是"看谁持有它"。

我们用 MAT 的 Path to GC Root:

byte[7,000,000,000 bytes]
    ↑ referenced by
RequestContext.body
    ↑ referenced by  
ThreadLocal$ThreadLocalMap.Entry.value
    ↑ referenced by
Thread[name="http-nio-exec-42"]    ← GC Root

真相大白:

请求处理的 RequestContext 用 ThreadLocal 存了 request body
处理完后没有调 ThreadLocal.remove()
线程是 Tomcat 线程池里的——长期存活,永不回收
ThreadLocal Map 里挂着的 RequestContext 永远活着
RequestContext 持有的 body(可能 10MB)也永远活着

Tomcat 200 个线程 × 平均每个累积 ~30MB body = 6GB 泄漏

修复仅一行:

try {
    process(request);
} finally {
    contextHolder.remove();   // ★ ThreadLocal.remove
}

这次救火让我刻骨铭心地体会到:

GC 解决的是"垃圾"——没人引用的对象自动回收
GC 解决不了"还有人引用但业务已经不用"——这是泄漏的本质
泄漏的根因永远是"逻辑错误",工具能做的是"暴露"它

# 0.2 100MB/天泄漏

另一个故事。有一个 Android App,用户反馈"用一会儿就卡"。我们一开始查 CPU、查网络、查图片解码——都没头绪。

最后接 LeakCanary,扫描日志:

LEAK FOUND in com.app.MainActivity:
  static field MyManager.instance     ← 泄漏链起点
  ↓
  field MyManager.lastActivity
  ↓
  com.app.MainActivity                ← 应该被回收的 Activity
  
Retained: 18.3MB

根因:

public class MyManager {
    private static MyManager instance;       // ✓ 单例,OK
    private Activity lastActivity;           // ⚠️ 这里出问题
    
    public void init(Activity activity) {
        lastActivity = activity;             // ⚠️ 持有 Activity 引用
    }
}
用户从 MainActivity 跳转到 DetailActivity → MainActivity 应该被回收
但 MyManager.instance.lastActivity 还指向 MainActivity
→ MainActivity 永远活着
→ 持有的 Bitmap、View、ContextWrapper 全活着(18MB)

每跳转一次→留下一个 MainActivity 实例→慢慢卡爆

修复:

private WeakReference<Activity> lastActivity;

这就是 Android 圈最经典的内存泄漏模式——长生命周期对象持有短生命周期对象。

# 0.3 灵魂三问

这两次事故让我反复追问:

  1. 既然有 GC,为什么还会内存泄漏?这不是和"自动内存管理"的承诺矛盾吗? —— 这反映了 GC 的根本局限是什么?
  2. 为什么 LeakCanary、MAT、pprof 这些工具都依赖"引用链分析"? —— 这个方法论的物理基础是什么?
  3. 8GB 堆 dump 看起来"啥都有",凭什么能在分钟级找到泄漏? —— 工具是怎么从海量对象中"快速定位元凶"的?

# 0.4 五个递进追问

要把"内存泄漏诊断"讲透,需要递进回答:

  1. GC 看到的"活" ≠ 业务想要的"活"——这个鸿沟从哪来?
  2. 泄漏的几种"经典模式"——它们都是同一种本质?
  3. GC Roots 是什么——为什么追溯它就能找到泄漏?
  4. Heap Dump 的格式和算法——为什么 8GB 能秒级分析?
  5. Profiler 怎么"实时"采样——它会不会自己拖慢服务?

# 0.5 探索路径

# 0.6 为何值得讲透

我想抛三个问题:

  1. 为什么"诊断"是工程师的核心能力,但没有几个学校教? —— 因为它是经验型的,没有标准答案。
  2. 为什么 LeakCanary 在 Square 内部诞生时,几乎"重新定义"了 Android 性能优化? —— 因为它把"事后查"变成"实时报"。
  3. 为什么 Go pprof 和 Java MAT 设计哲学完全不同? —— 一个是"采样统计",一个是"快照分析",反映了语言运行时的不同性格。

读完本章你会懂:诊断 = 让看不见的变成看得见的——而每种工具都是一面"特殊的眼镜"。

# 0.7 泄漏五语言对照

§0.1 和 §0.2 是 Java/Android 的版本——但**"还可达但业务不再需要"** 这件事,所有语言都有。下表把五种语言里最高频的泄漏骨架平铺,能看出 GC 机制对泄漏类型的"偏好":

语言 标志性泄漏范式 典型病灶 标志性诊断工具
Java ThreadLocal 不清 / static 集合不限 / EventBus 不反注册 "long-lived 持 short-lived" MAT / JProfiler / async-profiler
Go goroutine 阻塞导致栈不释放 + 闭包捕获大对象 + channel 满阻塞 goroutine 泄漏(更隐蔽,因为没"对象图") pprof (goroutine / heap / allocs)
JavaScript 闭包持有大数组 / DOM 引用 / 全局变量 / 监听器不 off 单页应用长跑后内存爬升 Chrome DevTools Memory / v8.getHeapStatistics
Python 循环引用 + __del__ 阻断 GC / 大字典 / numpy 数组 gc 模块默认配置下循环引用延迟回收 tracemalloc / objgraph / pympler
C / C++ malloc 没 free / 异常路径漏 free / 智能指针循环 "真泄漏"——无 GC 兜底 Valgrind / AddressSanitizer / heaptrack

两条普适规律:

规律 1(有 GC 语言的泄漏 = 业务语义错误):
  Java/Go/JS/Python 的泄漏不是"忘 free",而是"逻辑上持有太久"。
  诊断要找"谁活得太久"——LeakCanary、MAT、pprof 都基于这个范式。

规律 2(无 GC 语言的泄漏 = 内存所有权错误):
  C/C++/部分 Rust unsafe 的泄漏是"真的丢了引用"——
  malloc 后地址被覆盖、early return 漏 free、异常路径漏 delete。
  诊断要"插桩跟踪所有 malloc/free 配对"——Valgrind/ASan 是这个范式。

特别提醒:Go 的"goroutine 泄漏"是独一档的:

// 经典反例:channel 写入端死了,但读取端永远等
func leak() {
    ch := make(chan int)
    go func() { v := <-ch; use(v) }()  // 这个 goroutine 永远阻塞
    // 函数返回,ch 没人写,goroutine 永远活着 → 它持有的栈、闭包、变量全泄漏
}

这不是"对象图泄漏"——是"执行流泄漏"。Java 没有这个范式(线程是显式的),但 Go/Kotlin 协程都有。诊断要用 pprof goroutine 而不是 pprof heap。


# 01.内存泄漏本质

# 1.1 GC活与业务活

这是 GC 时代内存泄漏的本质矛盾:

GC 的判断标准(机械):
  对象从 GC Root 可达 → 活
  
业务的判断标准(语义):
  对象在未来还会被使用 → 活

这两者永远有鸿沟——GC 不知道"业务上是否还需要",只知道"是否可达"。

§0.3 第一题答案——GC 解决的是"自动找垃圾",但什么是"垃圾"由可达性定义;只要还可达,GC 就当成"活",哪怕业务已经不要了。

# 1.2 泄漏四种范式

经过多年踩坑,我把 GC 时代的内存泄漏归纳为 4 种范式:

# 范式 1:集合无界增长

// ❌ 缓存无 LRU 上限
private static Map<String, byte[]> cache = new HashMap<>();

public byte[] load(String key) {
    if (!cache.containsKey(key)) {
        cache.put(key, loadFromDisk(key));   // 永远在塞,从不淘汰
    }
    return cache.get(key);
}

症状:每个独特的 key 都会留下一份数据——永远不释放。

修复:

private static Map<String, byte[]> cache =
    new LinkedHashMap<String, byte[]>(16, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry e) {
            return size() > 1000;   // ★ 限制大小
        }
    };

// 或用 Caffeine、Guava Cache
Cache<String, byte[]> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterAccess(10, TimeUnit.MINUTES)
    .build();

# 范式 2:静态字段持有

// ❌ 静态字段持有"会变化"的对象
public class Holder {
    private static List<User> users = new ArrayList<>();
    
    public void addUser(User u) {
        users.add(u);   // 永远只增不减
    }
}

症状:static 是 GC Root——其引用的所有内容永生。

修复:避免静态字段持有"业务会变化"的对象。

# 范式 3:注册中心未反注册

// ❌ 注册了监听器但忘记取消
EventBus.register(this);
// 直到 EventBus 自己消亡,this 就活着

症状:观察者模式经典坑——subscribe 但忘了 unsubscribe。

修复:成对出现 register/unregister,配合 try-finally 或 RAII。

# 范式 4:闭包/内部类持有外部

// ❌ 在 Activity 中启动延迟任务
fun onCreate() {
    Handler().postDelayed({
        updateUI()   // 闭包隐式持有 Activity
    }, 60_000)
}
// 用户立刻退出 Activity → 但闭包持有 → Activity 不能回收

修复:

val handler = Handler()
val task = Runnable { updateUI() }
handler.postDelayed(task, 60_000)

override fun onDestroy() {
    handler.removeCallbacks(task)   // 取消延迟任务
}

# 1.3 四范式共同本质

这四种范式看似不同,但本质上都是同一句话:

"长生命周期"对象持有了"短生命周期"对象。

范式 1:static 缓存(永生)持有 byte[](应该短命)
范式 2:static 字段(永生)持有 User(应该短命)
范式 3:EventBus(永生)持有 Listener(应该和 Activity 同寿)
范式 4:Handler(持有 Looper,永生)持有 Closure(应该和 Activity 同寿)

这就是泄漏诊断的"思维模型"——永远找"谁活得太久了"。

# 1.4 资源vs内存泄漏

容易混淆的概念:

类型 例子 后果
内存泄漏 对象不释放 内存涨 → OOM
资源泄漏 文件描述符、Socket、数据库连接不关闭 FD 用尽、连接池耗尽
句柄泄漏 OS 级别句柄(HANDLE)不关闭 句柄耗尽

这三种泄漏经常同时发生——比如 InputStream 没关,既泄漏了对象,又泄漏了文件 FD。

通用解药:try-with-resources / RAII / context manager。


# 02.引用链分析

# 2.1 GC Roots起点

§0.4 第三题。GC Roots 是什么?

GC 把"绝对存活"的几类对象作为根:
  1. 当前线程栈帧中的局部变量
  2. 静态字段(static field)
  3. JNI 引用(native 代码持有)
  4. 系统类加载器(boot ClassLoader)
  5. 同步监视器(synchronized 持有的对象)
  
从这些根出发,可达的对象都"活"

所以泄漏分析的关键步骤:

1. 找到泄漏对象(如那 6GB 的 byte[])
2. 追溯它的"path to GC Root"
3. 看这条路径上"哪一段不该存在"
4. 修复那一段

# 2.2 Shallow与Retained

MAT 等工具会显示两个尺寸概念:

指标 含义
Shallow Size 对象自己占的字节(不含其引用的对象)
Retained Size 该对象死掉后能"释放"多少字节(包含其独占的引用链)

典型例子:

class A {
    private B b = new B();   // B 实例 100MB
}

A a = new A();
// a 的 Shallow Size:~16 字节(A 的对象头 + b 引用)
// a 的 Retained Size:~100MB(如果只有 a 引用 B)

找泄漏看 Retained Size——那 6GB 的 byte[] 自己 Shallow 就是 6GB,但 ThreadLocal Map 的 Retained Size 才是真正告诉你"删掉这个能回收 6GB"的指标。

# 2.3 支配树

§0.4 第四题。MAT 8GB dump 秒级找泄漏的"魔法"——Dominator Tree。

支配关系:

节点 A "支配" 节点 B:
  从 GC Root 到 B 的所有路径都必经 A
  
含义:A 死了,B 必然死

支配树:以 GC Root 为根,每个节点的父节点是它的"直接支配者"。

支配树(重新组织):
Root → A → C → E
         → D
       → B

如果 A 死 → C/D/E 全死
如果 C 死 → E 不一定死(D 还活着,D 也指向 E)

→ 但如果 D 也死(A 死会带走 D),E 才死

支配树的工程意义:

"删掉 A 能回收多少"= A 的子树总大小 = A 的 Retained Size
排序:找 Retained Size 最大的 → 最可疑的泄漏候选

这就是 MAT 能在分钟级处理 8GB dump 的算法基础——支配树构建是 O(N log N),远比朴素遍历快。

# 2.4 GC Root路径

def path_to_root(target):
    queue = [(target, [target])]
    visited = {target}
    
    while queue:
        node, path = queue.pop(0)
        if is_gc_root(node):
            return path
        
        for parent in incoming_refs(node):
            if parent not in visited:
                visited.add(parent)
                queue.append((parent, path + [parent]))
    
    return None

复杂度:O(N + E),其中 N 是对象数,E 是引用边数。

MAT 的优化:建索引让 incoming_refs 是 O(1) 查询。


# 03.Heap Dump原理

# 3.1 触发时机

# 主动 dump
jmap -dump:live,format=b,file=heap.bin <pid>

# 自动:OOM 时 dump
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dumps ...

# 通过 jcmd
jcmd <pid> GC.heap_dump /tmp/heap.bin

# 3.2 STW 代价

Heap Dump 不是"快照"——它需要遍历整个堆。期间通常要 STW:

1. JVM 暂停所有应用线程(safepoint)
2. 遍历所有对象,写入文件
3. 恢复线程

代价:

8GB 堆 → dump 时间约 30-60 秒
期间应用 100% 暂停!

生产经验:

不要随便 dump 大堆——会让服务"假死"
配置 OOM 自动 dump 时,目录磁盘要够(dump 文件 = 堆大小)
高峰期不 dump,低峰期或维护窗口

# 3.3 HPROF 格式

JVM Heap Dump 的标准格式:

HPROF Header
[String table]                ← 字符串池
[Class info]                  ← 类元数据
[Object instances]
  for each object:
    object id, class id, fields
[GC Roots]                    ← 标记哪些是根

HPROF 设计的精妙:

对象 ID 是"地址"——可以唯一定位
引用通过 ID 表示——重建对象图只需 ID 查表
GC Roots 单独标记——分析起点明确

# 3.4 增量流式dump

8GB dump 会让服务停 1 分钟——这在生产环境通常不可接受。

演进:

Java 11+:JFR(Java Flight Recorder)支持持续记录
  低开销(< 1%)
  可以记录"分配热点"等信息
  
但 JFR 不能替代 Heap Dump——分析"当前内存"还得用 dump

Live Heap Dump(OpenJ9 / Azul):

不需要 STW
利用并发标记的快照
代价:dump 期间内存用量翻倍

# 04.Profiler原理

# 4.1 采样与精确

采样式(Sampling):

每隔 N 毫秒"快照"一次调用栈
统计哪个方法最频繁出现 → 那是热点

精确式(Tracing/Instrumentation):

对每次方法调用、每次 new 都打点
精确但开销大

对比:

方式 精度 开销
采样 统计意义 1-3%
精确 100% 50%-300%

生产用采样——开销可接受。

# 4.2 AllocationTracking

§0.4 第五题。Profiler 怎么"看到"分配?

字节码插桩:

// 原始字节码:
new Foo
dup
invokespecial <init>

// 插桩后:
new Foo
dup
invokestatic Profiler.recordAllocation     ← 插入这一行
dup
invokespecial <init>

性能代价:每次 new 多一次方法调用——5-30% 开销。

JVMTI 的"采样分配"(JDK 11+):

JVM 内部按频率采样(如每 512KB 分配采一个)
不需要插桩
开销 < 1%
适合生产环境

# 4.3 火焰图设计

火焰图的精妙:

横轴:栈中各方法的"占比"(按 CPU 时间或分配量)
纵轴:调用栈深度
颜色:方法类型(用户代码 vs JVM vs Native)

→ 一眼看出热点:宽 = 多消耗
→ 一眼找到调用路径:纵向延伸 = 调用关系

Brendan Gregg 2011 年发明火焰图——彻底改变了性能分析:

之前:看一堆百分比表格,难以理解全貌
之后:一张图直观看出热点路径

# 4.4 Async Profiler

JVM 性能分析的"标准答案"——async-profiler:

./async-profiler -d 60 -f flame.html <pid>

核心优势:

1. 用 perf_events 采样(不依赖 safepoint)
2. 能采 CPU、Allocation、Lock、Wall Clock
3. 直接生成火焰图
4. 开销 < 1%

这是生产环境的"瑞士军刀"。


# 05.移动端的特殊难题

# 5.1 LeakCanary革命

§0.6 第二题答案。LeakCanary 是 Square 公司开源的 Android 泄漏检测工具,几乎"重新定义"了 Android 性能优化。

核心原理——WeakReference + ReferenceQueue:

// 1. Activity 销毁时
override fun onDestroy() {
    super.onDestroy()
    LeakCanary.watch(this)
}

// 2. LeakCanary 内部
fun watch(activity: Activity) {
    val ref = WeakReference(activity, refQueue)
    expectedRefs.add(ref)
}

// 3. 5 秒后检查
fun check() {
    System.gc()
    // 等一会,让 GC 跑完
    
    val collected = pollFromQueue(refQueue)
    val leaked = expectedRefs - collected
    
    for (ref in leaked) {
        // 这个 ref 应该被回收但没回收 → 泄漏!
        dumpHeap()              // 触发 Heap Dump
        analyzePathToRoot(ref)  // 找泄漏路径
        notify()                // 通知开发
    }
}

关键技术点:

1. 用 WeakReference——不阻止 GC
2. 用 ReferenceQueue——GC 回收时会通知
3. 主动 System.gc()——加快验证
4. 触发 Heap Dump 后离线分析路径

为什么 LeakCanary 这么受欢迎:

之前:发布前测试时偶尔做一次 dump 分析(事后诸葛亮)
LeakCanary:开发期实时报告泄漏(事中预防)

→ 把"罕见的、专家级"的诊断变成"日常的、自动的"流程

# 5.2 iOS MLeaksFinder

iOS 没有 GC(用 ARC——引用计数),泄漏更隐蔽:

// 经典坑:循环引用
self.block = ^{
    [self doSomething];   // self 持有 block,block 持有 self
};

MLeaksFinder(微信开源)的思路:

1. ViewController.dismiss 后
2. 等 2 秒
3. 用 weak 检查它是否真的释放了
4. 没释放 → 警告

对比 LeakCanary:

LeakCanary:依赖 GC 机制(WeakRef + Queue)
MLeaksFinder:依赖 ARC + 延迟检查

→ 两者实现机制不同,但思路一致:检测"该死的对象没死"

# 5.3 Valgrind与ASan

C/C++ 没有 GC——必须 free。Valgrind:

valgrind --leak-check=full ./myapp

输出:

==12345== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 2
==12345==    at 0x4C2E0BF: malloc (in vgpreload_memcheck)
==12345==    by 0x40058A: process (foo.c:42)
==12345==    by 0x4006A0: main (foo.c:80)

精确定位到行——但代价是程序慢 10-50 倍。

AddressSanitizer(ASan):

clang -fsanitize=address ...

核心:编译期插桩,运行时检查。性能损失只有 2-3 倍——生产前测试可接受。


# 06.跨平台诊断工具对照

# 6.1 工具矩阵

平台 主力工具 特色
JVM MAT, JProfiler, async-profiler, JFR 生态成熟,Dominator Tree
Android LeakCanary, Android Studio Profiler 实时报警
iOS Instruments, MLeaksFinder Allocations, Leaks
Go pprof 采样统计
Node.js heap snapshot, clinic.js V8 工具链
Python tracemalloc, objgraph 引用图可视化
C/C++ Valgrind, ASan 精确但慢
Rust heaptrack, valgrind 编译期消灭多数

# 6.2 pprof vs MAT

Go pprof——采样统计哲学:

go tool pprof http://localhost:6060/debug/pprof/heap
默认每分配 512KB 采一个样
统计"哪个 stack trace 分配最多"

输出:
  flat   flat%   sum%    cum   cum%
   2GB  60.00%  60.00%  2GB  60.00%   loadFromDB
 800MB  20.00%  80.00%  800MB 20.00%   parseRequest
 ...

Java MAT——快照分析哲学:

基于 Heap Dump(一个时刻的完整快照)
通过 Dominator Tree、Path to GC Root 找"具体的对象"

两种哲学的根源:

Go:runtime 控制力强,采样开销低 → 倾向"持续轻采样"
Java:堆大但分析丰富 → 倾向"事后深度分析快照"

Go pprof 像"心电图"——持续监测趋势
Java MAT 像"CT"——拍一张图细看

# 6.3 Node heap

const v8 = require('v8');
v8.writeHeapSnapshot('./snap.heapsnapshot');
// 用 Chrome DevTools 打开

JavaScript 闭包泄漏经典坑:

function setup() {
    const big = new Array(1_000_000);
    return function() {
        // 即便不用 big,闭包仍然持有!
        return 1;
    };
}
const fn = setup();   // big 永远活着

修复:

function setup() {
    const big = new Array(1_000_000);
    const result = compute(big);   // 用完
    big = null;                    // 显式断开
    return function() { return result; };
}

# 6.4 Python追踪

import tracemalloc
tracemalloc.start()

# 你的代码

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

for stat in top_stats[:10]:
    print(stat)

Python 的特殊难题——循环引用:

class Node:
    def __init__(self):
        self.next = None

a = Node()
b = Node()
a.next = b
b.next = a   # 循环!

del a
del b
# 引用计数都不为 0 → 不会立刻回收
# Python 的循环垃圾收集器会处理(但有延迟)

# 6.5 采样vs快照

§6.2 已经对比过 Go pprof 和 Java MAT——但所有诊断工具其实都可以放在两条轴上:

                    精确 ▲
                         │
              MAT  ●     │     ● JFR (Java Flight Recorder)
        Valgrind  ●      │     ● Heap Snapshot (V8)
       LeakCanary ●      │     ● tracemalloc (Python)
                         │
   事后 ◀─────────────────┼─────────────────▶ 实时
                         │
                         │     ● pprof (Go heap)
                         │     ● async-profiler (Java)
                         │     ● ARC dealloc 监控 (Swift)
                         │     ● py-spy
                    采样 ▼

两条轴的含义:

横轴(事后 vs 实时):
  事后 = 需要先收集数据(dump/snapshot),再离线分析——精度高、对线上无开销
  实时 = 程序运行时持续采样输出——开销低、能看趋势但精度有限

纵轴(采样 vs 精确):
  精确 = 记录每个对象、每次分配——能看到"具体对象 X 被谁持有"
  采样 = 按概率/间隔取样——能看到"哪类分配最多"、"哪个 stack 占大头"

工具选型的实战法则:

"我知道服务有泄漏但没事故"                  → 实时采样工具(pprof / async-profiler)
"凌晨爆 OOM 已死掉,留下 heap dump"          → 事后精确工具(MAT / DevTools Memory)
"性能差但不知道是不是 GC"                    → 实时采样(JFR / py-spy)
"知道是泄漏但找不到对象"                    → 事后精确(MAT 找 Path to GC Root)
"想做持续监控(不允许停机)"                → 实时采样(生产 JFR 永远开 0.5% 采样)

3 条工具使用经验:

1. 生产环境永远开"低成本实时采样" + "高成本事后快照按需触发"两层组合。
   Java:JFR(< 1% 开销,常开) + jmap(出问题时触发)。
   Go:pprof 端点(常开) + heap profile(出问题时拉)。

2. dump 文件的体积是堆的 80-120%——8GB 堆 dump 文件接近 10GB,
   传输是大问题。Java 用 jhsdb / heaphero 在线分析、Go 直接用 -alloc_objects
   而非 -alloc_space,可以避免下载全量。

3. 永远不要在 GC 配对的事故现场重启服务——
   重启后所有 root chain 都没了,诊断信息归零。
   先 jstack + jmap + GC 日志收齐,再考虑救火。

# 07.经典陷阱反模式

# 7.1 监听器忘反注册

// ❌ EventBus.register 后忘记 unregister
class MyFragment extends Fragment {
    @Override
    public void onCreate(Bundle b) {
        EventBus.getDefault().register(this);
    }
    // 没写 onDestroy 里的 unregister
}

修复:

@Override
public void onDestroy() {
    super.onDestroy();
    EventBus.getDefault().unregister(this);
}

# 7.2 缓存无界增长

// ❌ 无上限缓存
private Map<String, Object> cache = new ConcurrentHashMap<>();

修复:用有限大小 + 过期策略的缓存。

# 7.3 ThreadLocal未清

// ❌ 线程池里的线程长寿,ThreadLocal 不清就泄漏
private static ThreadLocal<Context> context = new ThreadLocal<>();

void process(Request req) {
    context.set(new Context(req));
    // 处理...
    // 没 remove
}

修复:

try {
    context.set(new Context(req));
    // 处理
} finally {
    context.remove();
}

# 7.4 单例持Activity

// ❌ 单例长生命周期,持有短生命周期 Activity
public static MyManager instance;
private Context context;

public static MyManager getInstance(Context c) {
    if (instance == null) instance = new MyManager(c);   // ⚠️ 如果 c 是 Activity
    return instance;
}

修复:

public static MyManager getInstance(Context c) {
    if (instance == null) instance = new MyManager(c.getApplicationContext());   // ★
    return instance;
}

# 7.5 内部类隐式持有

// ❌ 非静态内部类持有外部 this
class MyActivity extends Activity {
    private Handler handler = new Handler() {   // 非静态内部类
        @Override
        public void handleMessage(Message m) { ... }
    };
}

修复:

private static class SafeHandler extends Handler {
    private final WeakReference<MyActivity> ref;
    
    SafeHandler(MyActivity a) {
        ref = new WeakReference<>(a);
    }
}

# 7.6 监控指标缺失

生产环境必须有以下监控:

JVM 堆使用率(Old Gen 涨势)
GC 频率和耗时
Metaspace 增长(class 泄漏)
线程数(线程泄漏)
文件描述符数(FD 泄漏)

告警阈值:

Old Gen > 80% 持续 5 分钟 → WARN
Full GC 每分钟 > 1 次 → ERROR
线程数 > 上限的 80% → WARN

# 7.7 生产dump大堆

dump 8GB 堆 → STW 30 秒
对延迟敏感的服务直接挂掉

正确做法:

1. 先用 jstat / 监控指标观察
2. 在低峰期 dump
3. 优先 dump live(jmap -dump:live)排除 dead 对象
4. 或者用 JFR 持续记录,避免单次大 dump

# 08.综合案例串讲

§00 已经铺好了"凌晨告警"的故事开头,前面 7 节把"本质 / 引用链 / Heap Dump / Profiler / 移动端 / 工具 / 陷阱"逐项拆开。这一节把它们串成一条真实可操作的 24 小时排查时间线——一个工程师从接到告警到提交修复 PR 的全过程。

# 8.1 时间线总览

# 8.2 电商搜索背景

服务:       商品搜索 API(Spring Boot + ES)
机器:       16 核 / 32GB / JDK 21
JVM:        -Xms16g -Xmx16g -XX:+UseG1GC
运行时长:   每次启动后稳定 4-5 天就 OOM 重启
告警形式:   ProcessExited(137) + 容器 OOM Killer 日志

业务诉求:先恢复线上(重启),但必须找到根因(团队连续 3 周受困)。

# 8.3 T+0告警大盘

凌晨 2:00 PagerDuty 响起。值班工程师 SSH 上去第一步——不要急着 dump,先快速判断属于哪类问题:

# Step 1:进程还在不在?
$ jps
   12345 SearchApplication   ← 还活着,但已经被 OOM Killer 选中过 1 次

# Step 2:堆使用情况
$ jstat -gc 12345 1000 5
  S0C    S1C    S0U    S1U      EC       EU         OC          OU       MC     MU    YGC  YGCT  FGC  FGCT
 64.0   64.0    0.0   64.0   2048.0   1024.5   14336.0    14182.7   ...   42   1.234  187  98.5
                                                            ↑↑↑↑↑↑
                                            ⚠️ Old Gen 14182/14336,几乎打满
$ jstat -gc 12345 1000 5
                                                          ↑↑↑↑↑↑
                                            14195 → 14201 → 14210,缓慢爬升

关键判断:Old Gen 缓慢上涨且 Full GC 后不下降 → 典型泄漏特征(不是分配过快,而是回收不掉)。

对应章节:§01 内存泄漏的本质——"可达但无用",对照 §02 GC Roots 链不断的现象

# 8.4 T+1抓Heap Dump

# Step 3:先 live dump(排除 dead 对象)减小 dump 文件
$ jmap -dump:live,format=b,file=/data/heap-$(date +%Y%m%d-%H%M).hprof 12345
   Heap dump file created [3.2 GB in 47.8 secs]    ← live 8G heap → 3.2G dump

# Step 4:上传到分析机器
$ scp heap-20240801-0258.hprof analyst@analysis:/data/

为什么用 live:避免把临时垃圾也 dump 出来——8GB 全 dump 要 90 秒 STW,业务受不了。live 选项让 jmap 先做一次 Full GC 再 dump,文件减小 60%+。

对应章节:§03 Heap Dump 的工程原理 + §07 陷阱"jmap dump 触发 STW"

# 8.5 T+2 MAT支配树

把 dump 文件扔进 MAT(Memory Analyzer Tool):

1. File → Open Heap Dump → heap-20240801-0258.hprof
2. 等待索引(约 5-10 分钟)
3. 选 "Leak Suspects Report"——MAT 自动给 Top 嫌疑

MAT 输出:

Problem Suspect 1
─────────────────
1024 instances of "java.lang.Thread", loaded by "<system class loader>"
occupy 11,856,452,168 (84.3%) bytes.

These instances are referenced from one instance of:
  "java.lang.ThreadGroup"
  → loaded by "<system class loader>"

Keywords: java.lang.ThreadLocal$ThreadLocalMap

11.8 GB 全在 1024 个 Thread 里——这就是著名的 ThreadLocal 泄漏 + 线程池组合拳。

# 8.6 T+3.5锁定真凶

右键 Suspect → Path to GC Roots → exclude weak/soft references:

Thread "tomcat-http-pool-987" 
  └─ ThreadLocalMap (entry count: 5)
      └─ Entry [3]
          ├─ key (WeakReference): SearchContext.CTX_HOLDER (ThreadLocal)
          └─ value (Strong): SearchContext
              └─ List<SearchResult>  (size = 50,000) ← ⚠️ 50000 条历史记录
                  └─ SearchResult × 50000
                      └─ List<Product>  (per result avg 200) ← ⚠️ 总计 1000 万对象

根因:

// 业务代码(出问题的)
public class SearchContext {
    private static final ThreadLocal<SearchContext> CTX_HOLDER = new ThreadLocal<>();
    private List<SearchResult> history = new ArrayList<>();   // ← 历史记录追加,从不清理

    public static SearchContext current() {
        SearchContext ctx = CTX_HOLDER.get();
        if (ctx == null) {
            ctx = new SearchContext();
            CTX_HOLDER.set(ctx);   // ← 设置后从不 remove()
        }
        return ctx;
    }

    public void addResult(SearchResult r) {
        history.add(r);   // ← 每次搜索都追加
    }
}

双重打击:

  1. ThreadLocal 不调 remove() → ThreadLocalMap 持有 SearchContext 永不释放
  2. Tomcat 线程池复用线程 → 每个线程的 ThreadLocalMap 累计所有它处理过的请求的 SearchContext
  3. 4 天 = 200 个工作线程 × 平均处理 5 万次搜索 = 1000 万 SearchResult 永远在堆里

对应章节:§02 引用链分析(Thread→ThreadLocalMap→Entry.value)+ §07 陷阱 7.4 "ThreadLocal 经典泄漏"

# 8.7 T+5 Fix提交

// 修复版本
public class SearchContext implements AutoCloseable {
    private static final ThreadLocal<SearchContext> CTX_HOLDER = new ThreadLocal<>();
    private List<SearchResult> history = new ArrayList<>();

    public static SearchContext current() { ... }

    @Override
    public void close() {
        history.clear();         // ← 显式清空集合
        CTX_HOLDER.remove();     // ← 关键:ThreadLocal.remove()
    }
}

// 调用方(统一用 try-with-resources)
@RequestMapping("/search")
public List<Product> search(String q) {
    try (SearchContext ctx = SearchContext.current()) {
        // ... 业务逻辑
        return result;
    }   // ← close() 被自动调用
}

// 兜底 Filter(防漏调)
public class ThreadLocalCleanFilter implements Filter {
    public void doFilter(...) {
        try {
            chain.doFilter(req, resp);
        } finally {
            CTX_HOLDER.remove();   // ← 双重保险
        }
    }
}

3 行核心修改 + 1 个兜底 Filter——根因消除 + 旁路防御。

# 8.8 T+12 JFR监控

修复部署后不能马上下班——必须用 JFR 持续录制 12 小时确认无泄漏:

# 启动 JFR 持续记录
$ jcmd <pid> JFR.start name=leak-check duration=12h \
       filename=/data/leak-check-$(date +%Y%m%d).jfr \
       settings=profile

# 12 小时后看 Allocation 与 Old Gen 趋势
$ jcmd <pid> JFR.dump name=leak-check
$ jfr summary leak-check.jfr | grep "Old Generation"

JMC(Java Mission Control)打开 JFR 文件:

Memory → Garbage Collections
   Old Gen Heap Used 趋势:
   T+0h: 4.2 GB
   T+4h: 4.4 GB
   T+8h: 4.3 GB         ← 正常波动
   T+12h: 4.5 GB        ← 没有持续上涨 ✓

对比修复前的趋势:

时间点 修复前(4 天周期) 修复后(12 小时)
启动 +0h 3.0 GB 3.0 GB
启动 +12h 8.1 GB ⚠️ 4.5 GB ✓
启动 +24h 11.2 GB ⚠️ 4.6 GB ✓(投影)
启动 +96h OOM ❌ 稳定 ✓(投影)

确认修复有效——可以下班了。

对应章节:§04 Profiler "持续采样" 模式 + §06 工具选型(JFR/JMC vs 单次 dump)

# 8.9 知识点回归

§00 凌晨告警            → 本案例的"事故起点",与故事开头闭环
§01 泄漏的本质          → "可达但无用"——SearchContext 被 ThreadLocalMap 持有但永远不被业务用
§02 引用链分析          → §8.6 的 Path to GC Root:Thread→ThreadLocalMap→Entry
§03 Heap Dump 工程原理   → §8.4 的 jmap -dump:live + STW 权衡
§04 Profiler 看分配      → §8.8 的 JFR 持续录制 + JMC 趋势图
§05 移动端特殊难题       → 本案例不涉及(后端场景),但思路相通:ThreadLocal 在 Android Looper 线程同样泄漏
§06 跨平台诊断工具       → MAT(Java)/JMC/JFR 在工具矩阵中的位置
§07 经典陷阱             → §7.4 ThreadLocal 反模式的实战版

# 8.10 一句话提炼

内存泄漏排查的 24 小时方法论:①先看大盘判断类型(jstat 1 分钟)→ ②抓 live dump(jmap 1 分钟)→ ③MAT Dominator Tree 找最大占比(5 分钟)→ ④Path to GC Root 锁定根因(10 分钟)→ ⑤改 3 行代码 + 1 个兜底 Filter(30 分钟)→ ⑥JFR 持续 12 小时验证(次日确认)。80% 的内存泄漏都是这套流程能搞定的,剩下 20% 是 Native / Direct Buffer,需要 §05 的移动端工具链。


# 09.一句话总结

# 9.1 三层认知阶梯

第一层(知其然):知道用 MAT / LeakCanary 找泄漏
  ↓
第二层(知其所以然):理解 GC Roots、Dominator Tree、Heap Dump 格式
  ↓
第三层(知其将所以然):能设计可观测系统、定制 Profiler、做容量规划

读完本章后,你应该能回答开头§0.3 提出的三个问题:

  1. 既然有 GC,为什么还泄漏? → GC 解决"不可达对象",但 GC 时代的泄漏是"还可达但业务不再需要"——这是逻辑错误,工具帮不了。
  2. 为什么都依赖"引用链分析"? → 因为找泄漏 = 找"谁持有这个本应消亡的对象",引用链是唯一答案。
  3. 8GB dump 怎么秒级分析? → Dominator Tree 算法 O(N log N),加上索引让 incoming reference 查询 O(1)。

# 9.2 诊断流程图

# 9.3 七字真言

  1. GC 不是免死金牌——逻辑泄漏 GC 看不见。
  2. 找泄漏看 Retained Size——不是看 Shallow。
  3. 永远顺着引用链向上——找"谁活得太久"。
  4. 静态字段是头号嫌疑——天然的 GC Root。
  5. 生命周期不匹配 = 泄漏——长寿持短命的对象。
  6. 生产环境慎用 dump——用 JFR 或采样。
  7. 配套监控不可缺——内存、GC、FD、线程数。

# 9.4 与下篇承接

至此第 4 卷"内存的真相"前 7 篇全部结束。我们走过了:

  • 4.1 虚拟内存与地址空间:内存抽象的根基
  • 4.2 内存模型技术设计:可见性的硬件基础
  • 4.3 堆和栈内存的设计:两种分配策略
  • 4.4 内存对齐与缓存局部性:性能的隐形约束
  • 4.5 内存回收机制设计:自动 GC 的工程
  • 4.6 多种引用技术设计:生命周期的精细控制
  • 4.7 内存泄漏与诊断原理:把"看不见"变成"看得见"

下一篇 4.8 数据拷贝设计原理 (opens new window) 是本卷的收束——讨论"内存搬运"的工程艺术:从浅拷贝/深拷贝、到 zero-copy、再到 mmap 和 sendfile。所有数据传输的性能优化,最终都归结为"少搬一次"。


# 🔗 延伸阅读

  • 同卷上篇:4.5 内存回收机制设计 (opens new window) | 4.6 多种引用技术设计 (opens new window)
  • 同卷下篇:4.8 数据拷贝设计原理 (opens new window)
  • 经典文献:
    • Eclipse MAT User Guide(Eclipse 官方)—— Dominator Tree 算法的权威解释
    • Java Performance: The Definitive Guide(Scott Oaks)—— 第 7 章 Heap 分析
    • Mature Optimization Handbook(Carlos Bueno)—— 性能诊断方法论
    • LeakCanary 源码与博客(Square)—— Android 泄漏检测工业级方案
    • Brendan Gregg's Flame Graphs(brendangregg.com)—— 火焰图发明者博客
    • Go pprof 文档(Google)—— Go 性能诊断官方指南
    • Valgrind Manual(valgrind.org)—— Native 内存诊断的圣经
上次更新: 2026/06/28, 17:55:19
6.多种引用技术设计
8.数据拷贝设计原理

← 6.多种引用技术设计 8.数据拷贝设计原理→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式