📄 一句话概括
用完的内存不还=内存泄露(房子越占越多直到系统卡死);谁来还?垃圾回收(GC)自动收房——C 程序员亲手还(free),Python/Java 自动还(引用计数+分代回收),但自动收房也有收不到的死角:你还攥着房号。
📝 本节和前面几节的关系
3-14 讲了【要房、住人】(内存分配与指针),这一篇讲【退房】。回收 4-6 的作用域(局部变量随栈弹出自动释放)与 3-16 的栈——栈上自动清,堆上靠 GC。
一、内存泄露:房子不还
程序向系统申请内存(C 的 malloc、Python 的对象创建),用完该还。不还的下场:
// C 语言的经典泄露
char* buf = malloc(1024); // 要了一间房
buf = malloc(2048); // 又要一间 —— 旧的房号被覆盖,1024 那间永远没人还
一次泄露 1KB 没感觉;服务器循环里每次泄露 1KB、跑一天——内存条被喝干,系统卡死、进程被杀。泄露的本质:内存还在被占用,但再也没人能找到它去还——不是【丢了】,是【赖着没人管】。
二、垃圾回收:自动收房的两种策略
C/C++:手动管理——malloc/free(或 new/delete)亲力亲为。权力大:精确控制时机;事故多:忘 free(泄露)、重复 free(崩)、用已还的房(野指针,回收 3-14)。
Python/Java:自动垃圾回收——运行时盯着对象,【没人用了】就收:
- 引用计数(CPython 主力):每个对象随身带一个数字=【几个人指着我】;数字归零(没人指了)立刻回收。简单、及时;但收不了互相引用的孤岛(A 指 B、B 指 A,外部谁都不指它们,计数却各为 1);
- 分代回收(补刀):对象按存活时间分三代,新对象在【第 0 代】频繁检查——活得越久的越少查(老对象多半还要活)。孤岛就靠这一手收走;
- Java 还有不同的回收器家族(分代+并发标记等),思想同源:找出【从活着的根出发再也摸不到】的对象——摸不到=垃圾。
三、Python 里照样能【制造泄露】
GC 管的是【没人引用】的垃圾——你还攥着引用,它就不是垃圾:
cache = []
def process(data):
result = data * 2
cache.append(result) # 只进不出:列表越攒越大
return result
for i in range(1_000_000):
process(i) # 内存蹭蹭涨——不是 GC 失职,是你赖着不放
这类【逻辑性泄露】是 Python 内存问题的主体:越攒越大的全局列表/字典、忘了关的文件与连接、循环引用+带 del 的对象。诊断工具:tracemalloc(看内存涨在哪)。
四、动手 Demo:引用计数现形
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2(a + getrefcount 的临时参数)
b = a # 两个人指同一对象
print(sys.getrefcount(a)) # 3
del b # 撤一个引用
print(sys.getrefcount(a)) # 2 —— 回到 2;a 和 b 全撤(计数归零)对象立刻回收
看到什么算成功:2、3、2。
读结果:计数随引用增减——引用计数归零的那一刻对象就地回收(CPython 即时性)。计数多出的 1 是 getrefcount 调用本身的临时引用,别大惊小怪。
五、30 秒实验
跑:
import tracemalloc
tracemalloc.start()
leak = []
for i in range(100_000):
leak.append("x" * 100)
current, _ = tracemalloc.get_traced_memory()
print("占用约", current // 1024, "KB") # 约 10 万 x 100 字节 ≈ 9765 KB
内存涨在明处——把 leak.append 那行删掉再跑,几乎不涨。30 秒学会第一件内存诊断工具。
六、常见误区
- 【有垃圾回收就不会内存泄露】——错。GC 只收【摸不到】的对象;全局列表越攒越大、字典只增不删、闭包攥着大对象——你摸得到的东西 GC 永远不碰,逻辑性泄露照样发生。
- 【Python 的 del 是删数据】——错。del 删的是引用(名字);最后一个引用没了对象才回收——【删名字】与【拆房子】是两步。
- 【手动管理是落后的设计】——分场景。C 的手动管理换来确定性(嵌入式、实时系统必须);GC 换来安全省心但偶尔停顿(回收时可能暂停程序)——工程取舍,无高下。
自测三问(点开对答案)
1. **内存泄露的本质是什么?** 答:内存仍被占用但再也无法被找到归还——不是数据丢了,是【赖着且无人能管】。 2. **CPython 的两大回收机制及分工?** 答:引用计数(归零立刻回收,主力)+分代回收(补刀收互相引用的孤岛,按存活时间分级检查)。 3. **为什么 Python 也可能内存泄露?** 答:GC 只收摸不到的对象;全局容器只增不删、连接不关、闭包攥着大对象——引用还攥着,GC 永远不碰。- ☐ 下一节(4-11 排序、选择排序、插入排序)
相关笔记
- 地址、指针 —— 要房与住人的机制课
- 作用域 —— 栈上自动回收的原理
- 迭代器 —— 省内存的生产端
- ⌨️ 编程原理 · 索引 —— 全课路线图