Python与Java垃圾回收机制:详细对比

作者:袖梨 2026-07-23

一、核心机制

Python(CPython)

Python 采用 "引用计数为主 + 分代标记-清除为辅" 的双轨制:

Python与Java垃圾回收机制详细对比

  • 引用计数:每个对象的 ob_refcnt 实时记录引用数,归零时立即释放
  • 分代 GC:仅处理容器对象(list、dict、自定义类实例等)的循环引用,是一个"补丁"角色。
  • 原子类型(int、str 等)不参与分代 GC,完全由引用计数管理。

Java(HotSpot JVM)

Java 采用 "纯可达性分析" 的单机制:

  • 不维护引用计数。垃圾回收完全依赖从 GC Roots 出发的可达性分析。
  • GC Roots 包括:虚拟机栈引用、静态变量、常量池引用、JNI 引用等。
  • 对象回收时机不确定,完全由 GC 收集器在特定条件下触发。

一句话概括

Python 像"随时擦桌子的主人"(引用计数立即清理),JVM 像"定期大扫除的保洁团队"(按策略批量回收)。

二、对象头与内存开销

维度Python(CPython)Java(HotSpot)
头部结构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_FINALIZEDMark 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 分代对比表

对比项PythonJava
分代数量3 代2 物理区 + 元空间
物理分隔无——仅逻辑链表分代有——Eden/S0/S1/Old 物理隔离
复制算法新生代使用复制(Eden→S0/S1)
回收频率控制简单阈值计数多级阈值 + 动态自适应(Ergonomics)
复杂度极低极高

四、GC 算法细节

Python 循环引用检测

Python 的循环 GC 是一个三步骤的标记清除过程:

  1. 计算内部引用:复制所有容器对象的 ob_refcntgc_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 算法核心差异

维度PythonJava
主要清理方式引用计数(实时、分散)GC 收集器(批量、集中)
GC 算法标记-清除(仅循环检测)标记-清除 / 复制 / 标记-整理 / 分代组合
内存碎片obmalloc 的 arenas/pools/blocks 管理,基本避免碎片取决于收集器:复制算法无碎片;标记-清除可能产生碎片
并发/并行——GC 执行时持有 GIL,阻塞所有线程现代收集器(G1/ZGC)支持并发标记、并发整理

五、底层内存分配器

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)。

对比

维度PythonJava
设计目标优化大量小对象分配(Python 一切皆对象)优化高吞吐并发分配
线程安全依赖 GIL 保证TLAB + CAS 无锁分配
大对象处理>512B 走 malloc直接进入老年代
内存归还 OSArena 级归还取决于收集器(G1 可归还、Shenandoah/ZGC 更好)

六、GC 收集器对比

Python:只有一个 GC 实现(两个变体)

构建类型特点
默认构建(GIL)分代收集,依赖 GIL 保证线程安全
free-threaded 构建(3.13+)非分代(每次全堆扫描),有两次"Stop The World"暂停来暂停其他线程

Python 的 GC 没有并发收集器,没有增量收集器,也没有低延迟收集器。执行 GC 时 GIL 被持有,所有 Python 线程阻塞。

Java:多款工业级收集器

收集器算法特点目标STW 时间
Serial单线程标记-复制(新生代)+ 标记-整理(老年代)客户端/小应用全程 STW
Parallel Scavenge多线程复制 + Parallel Old(标记-整理)吞吐量优先(JDK 8 默认)全程 STW,但多线程并行
CMS并发标记-清除低延迟(JDK 9 废弃,JDK 14 移除)仅初始标记+重新标记 STW
G1分区 Region + 并发标记 + 混合回收平衡吞吐与延迟(JDK 9+ 默认)可控目标暂停 -XX:MaxGCPauseMillis
ZGC染色指针 + 并发整理亚毫秒级暂停(JDK 15+ 生产)<1ms(与堆大小无关)
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注册 GC 回调
sys.getrefcount()查看引用计数

调优空间极小。 基本只有调整 set_threshold 三参数或直接 gc.disable()

Java

组件说明
收集器选择-XX:+UseSerialGC / -XX:+UseParallelGC / -XX:+UseG1GC / -XX:+UseZGC
堆大小-Xms / -Xmx / -Xmn
分代比例-XX:SurvivorRatio / -XX:NewRatio
晋升阈值-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)与并发性

维度PythonJava
STW 范围整个解释器——GC 持有 GIL 时,所有 Python 线程阻塞取决于收集器——现代收集器阶段性 STW,且只暂停 Java 线程
GC 并发——GC 代码不可与 Python 代码并发——CMS、G1、ZGC 支持并发标记和并发整理
对多线程影响严重——GIL 意味 GC 期间所有线程停顿可控——ZGC 可实现亚毫秒级 STW
free-threaded(3.13+)GC 时暂停所有其他线程("Stop The World"),但暂停时间通常很短同上

九、循环引用处理

维度PythonJava
发现机制引用计数无法处理 → 依靠分代 GC 的循环检测算法可达性分析天然解决——不可达的环整体回收
处理时机分代 GC 触发时(通常 gen0 -> gen1 -> gen2 晋升)取决于 GC 周期,不可达环一次性整体清理
弱引用weakref 模块,支持回调;不可达时 GC 负责清除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+)

  • 无 GIL 的多线程 Python。
  • GC 不分代(每次全堆扫描)。
  • 对象中使用 ob_gc_bits(1 字节标志位)取代 PyGC_Head
  • 使用 mimalloc 扫描堆发现已跟踪对象。
  • 软件预取(Software Prefetch)优化"标记存活对象"阶段(长生命周期对象 >200K 时启用)。
  • 两次"Stop The World"暂停,暂停所有其他线程。

十一、综合对比总表

对比维度Python(CPython)Java(HotSpot)
主力机制引用计数(实时清理)可达性分析(按需清理)
辅助机制分代标记-清除(处理循环引用)——(主机制直接处理)
内存释放时机立即、确定(引用归零即释放)延迟、不确定(等待 GC 触发)
分代数量3 代(逻辑链表)2 物理代(Young/Old)+ Metaspace
GC 算法标记-清除(单纯)复制 + 标记-清除 + 标记-整理(多种组合)
对象头开销(引用计数 + GC 链表 + 类型指针)(Mark Word + Klass Pointer)
收集器选择无——只有一个实现6+ 种收集器(Serial / Parallel / CMS / G1 / ZGC / Shenandoah)
并发 GC无(GIL 阻塞所有线程)有(并发标记 + 并发整理)
STW 时间取决于对象数量(通常毫秒级)亚毫秒级(ZGC)到秒级(Serial GC)
内存分配器obmalloc(arenas/pools/blocks,专为小对象优化)TLAB + 指针碰撞 / 空闲列表
循环依赖处理分代 GC 的循环检测算法(三步骤)主 GC 统一处理
调优难度极低(只有 gc.set_threshold极高(上百个参数,多款收集器可选)
弱引用weakref 模块WeakReference / SoftReference / PhantomReference
Finalizer__del__ + tp_finalize(PEP 442)finalize()(JDK 9 已废弃,推荐 Cleaner
监控工具gc 模块 + sys.getrefcountjps / 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 性能优化的核心技能。

十三、参考资料

  • 参考g众号:计算机支持的传播者

相关文章

精彩推荐