🐍Python 语言基础

内存管理与垃圾回收

难度:⭐⭐ | 高频指数:🔥🔥🔥

面试回答

常见问法

  • Python 怎么管理内存?
  • 引用计数有什么缺陷?怎么解决循环引用?
  • 分代垃圾回收是什么原理?
  • __del__ 什么时候会被调用?有什么坑?
  • weakref 弱引用有什么用?
  • 和 C++ 的 RAII / shared_ptr 有什么异同?

回答

Python 内存管理分三层:私有堆 + 引用计数 + 分代 GC

  1. 引用计数是主力机制:每个对象维护一个引用计数器,当计数归零时立即释放。优点是实时回收、延迟低;缺点是无法处理循环引用,且维护计数有额外开销。

  2. 分代垃圾回收(Generational GC)专门解决循环引用:将对象分为 0/1/2 三代,新对象在第 0 代,存活越久晋升到更高代。GC 频率:第 0 代最频繁,第 2 代最少。

  3. pymalloc:Python 自带的小对象内存池,对 ≤512 字节的对象使用 arena/pool/block 三级结构,减少系统调用。

一句话总结:引用计数管日常释放,分代 GC 兜底循环引用,pymalloc 优化小对象分配

追问

1)引用计数怎么观察?

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

注意 sys.getrefcount() 本身会临时增加一次引用。


2)什么操作会增加/减少引用计数?

增加:赋值、传参、放入容器、函数闭包捕获。 减少:del、变量重新赋值、离开作用域、从容器移除。


3)循环引用怎么产生的?

class Node:
    def __init__(self):
        self.parent = None
        self.children = []

a = Node()
b = Node()
a.children.append(b)
b.parent = a  # a -> b -> a,循环引用

del a, b  # 引用计数不会归零,需要 GC 介入

4)分代 GC 怎么工作?

import gc

# 查看各代阈值
print(gc.get_threshold())  # 默认 (700, 10, 10)
# 含义:第 0 代分配 700 次触发 GC;第 0 代 GC 10 次后触发第 1 代;以此类推

# 手动触发
gc.collect()  # 返回回收的不可达对象数量

# 查看某对象的引用者
gc.get_referrers(obj)

5)__del__ 有什么坑?

class Resource:
    def __del__(self):
        print("释放资源")

# 坑1:不保证调用时机(解释器退出时可能不调用)
# 坑2:循环引用时,GC 无法确定析构顺序,可能不调用 __del__
# 坑3:__del__ 里抛异常会被忽略
# 坑4:复活对象(在 __del__ 里把 self 赋给全局变量)

工程建议:不要依赖 __del__ 管理资源,用上下文管理器(with)代替


6)weakref 弱引用有什么用?

import weakref

class Cache:
    pass

obj = Cache()
ref = weakref.ref(obj)

print(ref())   # <Cache object>
del obj
print(ref())   # None,不阻止 GC 回收

典型场景:

  • 缓存(不希望缓存阻止对象被回收)
  • 观察者模式(避免观察者和被观察者循环引用)
  • WeakValueDictionary / WeakSet

7)和 C++ RAII / shared_ptr 的类比?

维度PythonC++
主力机制引用计数 + GCRAII + 智能指针
循环引用分代 GC 自动处理weak_ptr 手动打破
释放时机引用归零立即释放(多数情况)离开作用域立即析构
析构保证__del__ 不保证析构函数保证调用
资源管理最佳实践with 上下文管理器RAII

原理展开

1. 三层内存架构

┌─────────────────────────────────────┐
│  Layer 3: Object-specific allocators │  ← list, dict, tuple 各自的 free list
├─────────────────────────────────────┤
│  Layer 2: Python object allocator    │  ← pymalloc (≤512B)
├─────────────────────────────────────┤
│  Layer 1: Python raw memory allocator│  ← malloc/free 封装
├─────────────────────────────────────┤
│  Layer 0: OS memory allocator        │  ← 系统调用 (mmap/brk)
└─────────────────────────────────────┘

pymalloc 的 arena(256KB)→ pool(4KB,按大小分类)→ block(8~512B)结构,避免频繁 malloc/free。


2. 引用计数的实现

CPython 中每个对象头部都有 ob_refcnt 字段:

// CPython 源码简化
typedef struct {
    Py_ssize_t ob_refcnt;   // 引用计数
    PyTypeObject *ob_type;  // 类型指针
} PyObject;

每次赋值、传参等操作都会 Py_INCREF / Py_DECREF。当 ob_refcnt 降为 0,立即调用 tp_dealloc 释放。


3. 分代 GC 的标记-清除算法

GC 处理循环引用的核心步骤:

  1. 遍历第 N 代所有容器对象(只有容器才可能循环引用)
  2. 复制引用计数到临时字段 gc_refs
  3. 模拟删除:遍历每个对象的引用,将被引用对象的 gc_refs 减 1
  4. 标记gc_refs > 0 的对象从外部可达,标记为存活
  5. 清除gc_refs == 0 且不可达的对象,判定为垃圾
import gc

# 禁用自动 GC(性能敏感场景)
gc.disable()

# 手动在合适时机触发
gc.collect(generation=0)  # 只收集第 0 代
gc.collect()              # 收集所有代

4. 内存泄漏排查实战

import tracemalloc

# 启动内存追踪
tracemalloc.start()

# ... 业务代码 ...

# 查看内存快照
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
    print(stat)

配合 gc.set_debug(gc.DEBUG_LEAK) 可打印无法回收的对象;objgraph 库可可视化引用关系。


易错点

  • 以为 del 就是释放内存 del 只是解除名字绑定、减少引用计数,不一定立即释放(可能还有其他引用)。

  • 依赖 __del__ 做资源清理 不保证调用时机,循环引用时行为不确定。应该用 with 语句。

  • 以为 Python 没有内存泄漏 循环引用 + 全局变量持有 + 闭包捕获,都可能导致对象无法回收。

  • 混淆”引用计数归零”和”GC 回收” 引用计数归零是立即释放;GC 是周期性扫描处理循环引用。

  • 在多线程中手动调 gc.collect() GC 会暂停所有线程(STW),高频调用影响性能。

  • 以为 gc.disable() 后不会有任何回收 引用计数仍然工作,只是分代 GC 不再自动触发。


记忆技巧

  • 三层口诀:pymalloc 管分配,引用计数管释放,分代 GC 管循环。

  • 引用计数像投票:每多一个人引用就 +1,没人引用就淘汰。

  • 分代像学校:新生(gen0)考试最频繁,老生(gen2)很少被查。

  • __del__ 像遗嘱:不保证什么时候执行,别把重要事放里面。

  • weakref 像旁观者:看得到对象,但不会阻止它被回收。


面试速答版

Python 内存管理核心是引用计数 + 分代垃圾回收。引用计数是主力,对象引用归零立即释放,优点是实时性好;缺点是无法处理循环引用。分代 GC 专门解决循环引用,把对象分为 0/1/2 三代,新对象在第 0 代,GC 频率从高到低。底层还有 pymalloc 内存池优化小对象分配。工程上不要依赖 __del__ 管理资源,用上下文管理器;排查内存泄漏用 tracemalloc + gc 模块。和 C++ 对比,Python 的引用计数类似 shared_ptr,分代 GC 相当于自动版的 weak_ptr 打破循环。

Related · 语言基础