201_Python内存管理与GC¶
一句话概述¶
Python使用引用计数(reference counting)作为主要的内存管理机制,辅以分代垃圾回收(generational GC)处理循环引用,理解这套机制对编写内存高效的程序和排查内存泄漏至关重要。
核心知识点表格¶
| 知识点 | 说明 |
|---|---|
| 引用计数 | 每个对象维护一个引用计数器,归零时立即释放 |
| 循环引用 | 两个对象互相引用,引用计数永远不归零 |
| 分代GC | 将对象分为0/1/2三代,年轻代更频繁扫描 |
| gc模块 | 控制垃圾回收行为的标准库模块 |
| del | 析构方法,对象被回收前调用 |
| weakref | 弱引用,不增加引用计数 |
| sys.getrefcount | 查看对象的引用计数 |
| 内存池(pymalloc) | 小对象的内存分配优化 |
步骤详解¶
第一步:引用计数机制¶
白话解释:Python给每个对象配了一个"计数器",有人引用它就+1,没人引用就-1。计数器变成0就说明没人用了,立刻回收内存。
import sys
# 查看引用计数
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 (a本身 + getrefcount参数)
b = a # 引用计数+1
print(sys.getrefcount(a)) # 3
c = [a, a] # 又+2
print(sys.getrefcount(a)) # 5
del b # -1
print(sys.getrefcount(a)) # 4
# 引用计数增加的场景:
# 1. 赋值: b = a
# 2. 作为参数: func(a)
# 3. 容器元素: lst = [a]
# 4. 属性引用: obj.attr = a
# 引用计数减少的场景:
# 1. del语句: del a
# 2. 变量重新赋值: a = None
# 3. 离开作用域
# 4. 容器删除元素
第二步:循环引用与垃圾回收¶
import gc
# 循环引用示例
class Node:
def __init__(self, name):
self.name = name
self.next = None
def __del__(self):
print(f"Deleting {self.name}")
# 创建循环引用
a = Node("A")
b = Node("B")
a.next = b
b.next = a # 循环引用!
# 删除外部引用
del a, b
# 此时A和B的引用计数都是1(互相引用)
# 引用计数不会变成0,需要GC介入
# 手动触发GC
gc.collect() # 输出: Deleting A, Deleting B
# 查看GC信息
print(gc.get_stats()) # 各代的收集统计
print(gc.get_threshold()) # 触发收集的阈值 (700, 10, 10)
# 调整GC行为
gc.set_threshold(500, 5, 5) # 更激进的收集
gc.disable() # 禁用自动GC(谨慎使用!)
gc.enable() # 启用
第三步:弱引用避免循环¶
import weakref
class ExpensiveObject:
def __init__(self, data):
self.data = data
# 正常引用
obj = ExpensiveObject("重要数据")
ref = weakref.ref(obj) # 弱引用不增加引用计数
print(ref()) # <ExpensiveObject object>
print(ref().data) # 重要数据
del obj
print(ref()) # None(对象已被回收)
# WeakValueDictionary:缓存的好帮手
class ImageCache:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_image(self, path):
img = self._cache.get(path)
if img is None:
img = self._load_image(path)
self._cache[path] = img
return img
def _load_image(self, path):
return f"Image({path})"
# 当外部不再引用Image时,缓存自动清理
第四步:内存分析与优化¶
import tracemalloc
import sys
# tracemalloc追踪内存分配
tracemalloc.start()
data = [list(range(1000)) for _ in range(1000)]
snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics('lineno')
for stat in top[:5]:
print(stat)
# 对象大小
print(f"int大小: {sys.getsizeof(42)} bytes")
print(f"空list: {sys.getsizeof([])} bytes")
print(f"空dict: {sys.getsizeof({})} bytes")
print(f"空str: {sys.getsizeof('')} bytes")
# __slots__节省内存
class WithoutSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
import sys
a = WithoutSlots(1, 2)
b = WithSlots(1, 2)
print(f"Without slots: {sys.getsizeof(a) + sys.getsizeof(a.__dict__)} bytes")
print(f"With slots: {sys.getsizeof(b)} bytes")
# slots通常节省40-50%内存
# 生成器vs列表(节省大量内存)
import sys
lst = [i**2 for i in range(1000000)] # ~8MB
gen = (i**2 for i in range(1000000)) # ~200 bytes
print(f"List: {sys.getsizeof(lst)} bytes")
print(f"Generator: {sys.getsizeof(gen)} bytes")
第五步:内存泄漏排查¶
import gc
import objgraph # pip install objgraph
# 查找最常见的对象类型
objgraph.show_most_common_types(limit=10)
# 查找增长的对象类型
objgraph.show_growth(limit=5)
# 查找循环引用
gc.collect()
garbage = gc.garbage # 无法回收的循环引用对象
# 使用memory_profiler逐行分析
# pip install memory_profiler
from memory_profiler import profile
@profile
def memory_intensive():
big_list = [i for i in range(1000000)]
del big_list
return "done"
# 命令行运行: python -m memory_profiler script.py
实战命令速查¶
# 引用计数
sys.getrefcount(obj)
# GC控制
gc.collect(); gc.get_stats(); gc.set_threshold(700, 10, 10)
# 弱引用
ref = weakref.ref(obj); weakref.WeakValueDictionary()
# 内存分析
tracemalloc.start(); snapshot = tracemalloc.take_snapshot()
# 命令行分析
# python -m memory_profiler script.py
# python -m tracemalloc script.py
面试常问点¶
Q1: Python的引用计数和垃圾回收是什么关系? A: 引用计数是主要的内存管理机制,对象引用计数归零时立即释放。分代垃圾回收是辅助机制,专门处理引用计数无法解决的循环引用问题。GC定期扫描对象图,检测和回收循环引用。
Q2: 为什么引用计数不能处理循环引用? A: 当A引用B、B引用A时,即使外部不再引用A和B,它们的引用计数也不会归零(各为1)。引用计数机制无法检测这种情况,需要GC的标记-清除算法来识别和回收。
Q3: __slots__如何节省内存? A: 普通Python对象使用__dict__(字典)存储属性,字典本身有额外开销(哈希表、预分配空间等)。slots__使用固定的C结构体存储属性,不创建__dict,省去了字典的开销。对于大量小对象可节省40-50%内存。
Q4: 分代GC的三代分别是什么? A: 第0代(新生代):新创建的对象,收集最频繁。第1代(中年代):经历过一次GC的对象。第2代(老年代):经历过多次GC的存活对象,收集最不频繁。假设:存活时间长的对象继续存活的概率高。
Q5: 如何检测和修复内存泄漏? A: (1)使用tracemalloc跟踪内存分配;(2)使用objgraph可视化对象引用图;(3)使用gc.get_objects()检查存活对象;(4)使用weakref替代强引用;(5)检查全局变量、缓存、回调中的引用。
易错点¶
- __del__与循环引用:有__del__方法的对象参与循环引用时,GC无法确定析构顺序,可能不被回收(Python 3.4前),放入gc.garbage
- 大量小对象:CPython的pymalloc对<512bytes的对象有优化,但释放后内存可能不归还OS
- 全局变量持有引用:全局字典/列表不断增长是常见泄漏源
- 闭包捕获变量:闭包会保持外部变量的引用,阻止回收
- C扩展模块泄漏:C扩展中的引用计数管理错误不被Python GC检测
补充知识¶
Python对象内存布局¶
| 对象类型 | 基本大小(64位) | 说明 |
|---|---|---|
| int (小) | 28 bytes | CPython小整数缓存[-5, 256] |
| float | 24 bytes | |
| str (空) | 49 bytes | +每字符1-4 bytes |
| list (空) | 56 bytes | +每元素8 bytes指针 |
| dict (空) | 64 bytes | 哈希表额外开销 |
| tuple (空) | 40 bytes | 比list小,不可变 |
| 自定义对象 | 48 bytes + dict | slots__可省去__dict |