跳转至

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)检查全局变量、缓存、回调中的引用。

易错点

  1. __del__与循环引用:有__del__方法的对象参与循环引用时,GC无法确定析构顺序,可能不被回收(Python 3.4前),放入gc.garbage
  2. 大量小对象:CPython的pymalloc对<512bytes的对象有优化,但释放后内存可能不归还OS
  3. 全局变量持有引用:全局字典/列表不断增长是常见泄漏源
  4. 闭包捕获变量:闭包会保持外部变量的引用,阻止回收
  5. C扩展模块泄漏:C扩展中的引用计数管理错误不被Python GC检测

补充知识

Python对象内存布局

对象类型基本大小(64位)说明
int (小)28 bytesCPython小整数缓存[-5, 256]
float24 bytes
str (空)49 bytes+每字符1-4 bytes
list (空)56 bytes+每元素8 bytes指针
dict (空)64 bytes哈希表额外开销
tuple (空)40 bytes比list小,不可变
自定义对象48 bytes + dictslots__可省去__dict