一、核心机制
Python(CPython)
Python 采用 "引用计数为主 + 分代标记-清除为辅" 的双轨制:
- • 引用计数:每个对象的
ob_refcnt 实时记录引用数,归零时立即释放。 - • 分代 GC:仅处理容器对象(list、dict、自定义类实例等)的循环引用,是一个"补丁"角色。
- • 原子类型(int、str 等)不参与分代 GC,完全由引用计数管理。
Java(HotSpot JVM)
Java 采用 "纯可达性分析" 的单机制:
- • 不维护引用计数。垃圾回收完全依赖从 GC Roots 出发的可达性分析。
- • GC Roots 包括:虚拟机栈引用、静态变量、常量池引用、JNI 引用等。
- • 对象回收时机不确定,完全由 GC 收集器在特定条件下触发。
一句话概括
Python 像"随时擦桌子的主人"(引用计数立即清理),JVM 像"定期大扫除的保洁团队"(按策略批量回收)。
二、对象头与内存开销
| | |
|---|
| 头部结构 | PyGC_Head 含 _gc_next、_gc_prev(各 8 字节)+ ob_refcnt(8 字节)+ ob_type(8 字节) | Mark Word(8/4 字节)+ Klass Pointer(8/4 字节,压缩后可 4 字节) |
| 关键字段 | ob_refcnt | Mark Word 存哈希码、GC 年龄(4 bit)、锁状态、偏向锁等 |
| 额外开销 | 大 | 小 |
| GC 专用空间 | _gc_prev 可被复用(存储标志位 PREV_MASK_COLLECTING、_PyGC_PREV_MASK_FINALIZED) | Mark Word 在 GC 时复用存储转发指针(用于复制/整理) |
Python 对象内存布局(默认构建)
+----------------------------------+ \
| *_gc_next | |
+----------------------------------+ | PyGC_Head(仅容器对象)
| *_gc_prev | | ← 可复用:存 flag + gc_ref
object -----> +----------------------------------+ /
| ob_refcnt | \ ← 引用计数(必须)
+----------------------------------+ | PyObject_HEAD
| *ob_type | |
+----------------------------------+ /
| ... |
为什么 Python 不能像 Java 一样放弃引用计数?
历史包袱 + C 扩展生态。NumPy、PyTorch 等 C 扩展高度依赖引用计数的确定性来及时释放非内存资源(GPU 显存、文件句柄)。如果改成纯标记-清除,这些扩展的内存管理会极其复杂。
三、分代回收机制
共同点
都基于弱代假说(Weak Generational Hypothesis):绝大多数对象"朝生夕死"。
Python 分代
| |
|---|
| 分代数量 | 3 代(generation 0 / 1 / 2) |
| 默认阈值 | 早期版本为 (700, 10, 10);Python 3.13+ 将 threshold0 提高至 (2000, 10, 10) |
| 触发条件 | 当 allocations - deallocations > threshold0 时触发 gen0 扫描 |
| 晋升规则 | 每次 GC 存活 → 升一代;gen2 对象不再晋升 |
| 扫描范围 | gen0 最频繁(只有新对象),gen1 稍少,gen2 很少——gen2 仅在 long_lived_pending / long_lived_total > 25% 时扫描 |
| free-threaded 构建 | 不使用 |
Java 分代(以 HotSpot 为例)
| |
|---|
| 分区模型 | 新生代(Young:Eden + S0 + S1)+ 老年代(Old)+ 元空间(Metaspace,JDK 8+) |
| 默认比例 | Eden : S0 : S1 = 8 : 1 : 1(-XX:SurvivorRatio=8) |
| 触发条件 | Eden 满 → Minor GC / Young GC;老年代满 → Major GC / Full GC |
| 晋升规则 | 对象在 S0/S1 每存活一次 Minor GC,年龄 +1;默认 15 岁升入老年代(-XX:MaxTenuringThreshold=15) |
| GC 类型 | Minor GC(仅新生代)、Major GC(老年代)、Full GC(整个堆 + Metaspace) |
Python vs Java 分代对比表
四、GC 算法细节
Python 循环引用检测
Python 的循环 GC 是一个三步骤的标记清除过程:
- 1. 计算内部引用:复制所有容器对象的
ob_refcnt 到 gc_ref;遍历每个容器,将其引用对象的 gc_ref 减 1。 - 2. 识别不可达对象:
gc_ref == 0 的对象移入"暂定不可达"列表;再遍历可达对象,把被可达对象引用的对象"救回"可达列表。 - 3. 销毁不可达对象:处理弱引用回调 → 调用
__del__ / tp_finalize → 处理复活对象 → 调用 tp_clear 打破循环 → 释放内存。
Java GC 算法
| | |
|---|
| Mark-Sweep(标记-清除) | | 标记存活对象后清除未标记的。缺点:产生内存碎片;效率不高。 |
| Copying(复制) | | 将存活对象从 Eden/S0 复制到 S1/S0,清空原区。优点:无碎片,只需移动指针。代价:浪费一半空间(S0/S1 互备)。 |
| Mark-Compact(标记-整理) | | 标记后把存活对象移动到内存一端,清理边界外。优点:无碎片。代价:移动成本高,STW 时间长。 |
| Generational(分代) | | 上述算法的组合使用——新生代用复制,老年代用清除/整理。 |
Python vs Java GC 算法核心差异
| | |
|---|
| | |
| | 标记-清除 / 复制 / 标记-整理 / 分代组合 |
| 由 obmalloc 的 arenas/pools/blocks 管理,基本避免碎片 | 取决于收集器:复制算法无碎片;标记-清除可能产生碎片 |
| 无 | |
五、底层内存分配器
Python:obmalloc(基于 malloc)
Arena(256KB 对齐)
├── Pool 1(4KB,同一 size class)
│ ├── Block 48B
│ ├── Block 48B
│ └── ...
├── Pool 2
└── ...
- • Arena:最大分配单元(256KB),按页边界对齐。空闲 Arena 可以真正归还 OS。
- • Pool:一个虚拟内存页(4KB),所有 block 属于同一 size class。
- • Block:最小分配单元,size class 从 8B ~ 512B。大于 512B 直接调
malloc。 - • freepools / usedpools:管理空闲/使用中的 Pool 链表。
Java:TLAB + 堆分区
- • TLAB(Thread Local Allocation Buffer):每个线程在 Eden 区有独立小缓冲区,避免锁竞争。
- • 堆分区:Eden 区大块连续分配(指针碰撞 Bump-the-Pointer)。
- • 大对象直接进老年代(
-XX:PretenureSizeThreshold)。
对比
| | |
|---|
| | |
| | |
| | |
| | 取决于收集器(G1 可归还、Shenandoah/ZGC 更好) |
六、GC 收集器对比
Python:只有一个 GC 实现(两个变体)
| |
|---|
| 默认构建(GIL) | |
| free-threaded 构建(3.13+) | 非分代(每次全堆扫描),有两次"Stop The World"暂停来暂停其他线程 |
Python 的 GC 没有并发收集器,没有增量收集器,也没有低延迟收集器。执行 GC 时 GIL 被持有,所有 Python 线程阻塞。
Java:多款工业级收集器
| | | |
|---|
| Serial | 单线程标记-复制(新生代)+ 标记-整理(老年代) | | |
| Parallel Scavenge | 多线程复制 + Parallel Old(标记-整理) | | |
| CMS | | | |
| G1 | | | 可控目标暂停 -XX:MaxGCPauseMillis |
| ZGC | | | |
| Shenandoah | | | |
七、调优与工具
Python
| |
|---|
gc | gc.set_threshold(t0, t1, t2)、gc.disable()、gc.collect() |
gc.get_stats() | 获取各代统计信息(collections、collected、uncollectable) |
gc.DEBUG_LEAK | |
gc.freeze() | 冻结对象到永久代(fork 前用,避免 copy-on-write) |
gc.callbacks | |
sys.getrefcount() | |
调优空间极小。 基本只有调整 set_threshold 三参数或直接 gc.disable()。
Java
| |
|---|
| 收集器选择 | -XX:+UseSerialGC / -XX:+UseParallelGC / -XX:+UseG1GC / -XX:+UseZGC |
| 堆大小 | -Xms |
| 分代比例 | -XX:SurvivorRatio |
| 晋升阈值 | -XX:MaxTenuringThreshold |
| GC 日志 | -Xlog:gc*(JDK 9+)、-XX:+PrintGCDetails(JDK 8) |
| 监控工具 | jps / jstat / jvisualvm / MAT / Arthas / GCViewer |
| 调优参数 | 上百个(如 -XX:MaxGCPauseMillis、-XX:GCTimeRatio、-XX:ParallelGCThreads、-XX:ConcGCThreads 等) |
调优空间极大。 选择合适的收集器 + 调优参数可以显著影响性能。
八、Stop The World(STW)与并发性
| | |
|---|
| 整个解释器——GC 持有 GIL 时,所有 Python 线程阻塞 | 取决于收集器——现代收集器阶段性 STW,且只暂停 Java 线程 |
| 无 | 有 |
| 严重 | 可控 |
| GC 时暂停所有其他线程("Stop The World"),但暂停时间通常很短 | |
九、循环引用处理
| | |
|---|
| 引用计数无法处理 → 依靠分代 GC 的循环检测算法 | |
| 分代 GC 触发时(通常 gen0 -> gen1 -> gen2 晋升) | |
| weakref | WeakReference / SoftReference / PhantomReference + ReferenceQueue |
十、Python 特有机制
1. 延迟 untrack(Delayed Untracking)
某些容器类型确定不能参与循环引用时,GC 会将其"untrack"(从跟踪链表中移除):
- • Tuple:创建时先跟踪,GC 周期中检查——如果所有元素不可跟踪,则 untrack。
- • Dict(3.14+):始终跟踪,不再懒跟踪(3.13 及之前用
_PyDict_MaybeUntrack 在 Full GC 时检查)。
2. 冻结机制(Freeze)
gc.freeze() 将所有已跟踪对象移入"永久代",后续 GC 不再扫描。用于 fork() 场景——避免子进程 GC 污染父进程对象的内存页(copy-on-write)。
3. free-threaded 构建(3.13+)
- • 对象中使用
ob_gc_bits(1 字节标志位)取代 PyGC_Head。 - • 使用 mimalloc 扫描堆发现已跟踪对象。
- • 软件预取(Software Prefetch)优化"标记存活对象"阶段(长生命周期对象 >200K 时启用)。
- • 两次"Stop The World"暂停,暂停所有其他线程。
十一、综合对比总表
| | |
|---|
| 主力机制 | | |
| 辅助机制 | | |
| 内存释放时机 | 立即、确定 | 延迟、不确定 |
| 分代数量 | | 2 物理代(Young/Old)+ Metaspace |
| GC 算法 | | |
| 对象头开销 | 大 | 小(Mark Word + Klass Pointer) |
| 收集器选择 | | 6+ 种收集器(Serial / Parallel / CMS / G1 / ZGC / Shenandoah) |
| 并发 GC | | |
| STW 时间 | | |
| 内存分配器 | obmalloc(arenas/pools/blocks,专为小对象优化) | |
| 循环依赖处理 | | |
| 调优难度 | 极低 | 极高 |
| 弱引用 | weakref | WeakReference / SoftReference / PhantomReference |
| Finalizer | __del__ | finalize() |
| 监控工具 | gc | jps / jstat / jvisualvm / MAT / Arthas |
| 设计哲学 | 简单、实时、兼容 C 扩展 | 极致吞吐量 / 低延迟、服务端优化 |
十二、实战启示
- 1. Python 中 不是删除对象——是减少引用计数。对象何时真正释放取决于引用计数是否归零,以及循环引用是否被 GC 检测到。
- 2. Python 不需要手动触发 GC——引用计数已经处理了绝大多数对象,
gc.collect() 仅在需要追踪内存泄漏或处理大量循环引用时使用。 - 3. Python 中循环引用不会被立即回收——必须等待分代 GC 触发。这也是为什么
weakref 在某些场景下很重要。 - 4. Java 需要选对收集器——批处理/服务端应用:Parallel GC(吞吐量优先);Web 服务:G1(平衡延迟);低延迟系统:ZGC(亚毫秒级暂停)。
- 5. Java 调优需要看 GC 日志——
-Xlog:gc* 是必备参数;GC 日志分析是 Java 性能优化的核心技能。
十三、参考资料
- • Real Python - Memory Management in Python