Python 与内存管理,第二部分:堆内存

发布: (2026年2月8日 GMT+8 05:08)
9 分钟阅读
原文: Dev.to

Source: Dev.to

(请提供需要翻译的正文内容,我才能为您完成简体中文的翻译。)

Python 的内存区段:对象的所在位置

当你运行一个 Python 程序时,操作系统会为该进程创建多个内存区域。
在 Linux 上,你可以使用下面的脚本检查这些区域(保存为文件后执行):

#!/usr/bin/env python3
import os, sys

def main():
    pid = os.getpid()
    with open(f"/proc/{pid}/maps") as f:
        for line in f:
            line = line.strip()
            parts = line.split()
            if len(parts) >>>> Found: {name} ({id(obj):#x}) in this block")

# Objects to track
vars = {
    "True": True,
    "small number": 4,
    "large number": 2**123,
    "small string": "a",
    "large string": "a" * 2000,
    "int class": int,
    "sys module": sys,
    "main function": main,
}

if __name__ == "__main__":
    main()

该脚本读取 /proc/<pid>/maps,打印每个内存块,并报告 vars 中的对象是否位于该块中。

下面是你最常会看到的几类内存区域的简要概述。

1. 可执行代码区(只读)

这些块包含 Python 解释器二进制文件以及它加载的所有共享库。
典型的权限是 r—p(只读)或 r‑xp(可读可执行)。

00400000-0041f000 r--p 00000000 08:11 545993 /usr/bin/python3.14
0041f000-006d2000 r-xp 0001f000 08:11 545993 /usr/bin/python3.14
006d2000-00945000 r--p 002d2000 08:11 545993 /usr/bin/python3.14
...
7f3d6c6ab000-7f3d6c6d1000 r--p 00000000 08:11 541316 /usr/lib/x86_64-linux-gnu/libc.so.6
7f3d6c6d1000-7f3d6c827000 r-xp 00026000 08:11 541316 /usr/lib/x86_64-linux-gnu/libc.so.6

这些区段是 Python 可执行文件或它依赖的共享对象的一部分。

2. 数据段(读写)

该区域保存属于 Python 可执行文件本身的读写数据,例如单例对象 True、小整数以及解释器启动时预先分配的内建类型。

00946000-00a85000 rw-p 00545000 08:11 545993 /usr/bin/python3.14
  >>>>> Found: True (0x95a0a0) in this block
  >>>>> Found: small number (0xa5bb08) in this block
  >>>>> Found: int class (0x958cc0) in this block

3. 堆区(动态分配)

由操作系统管理的堆(brk/sbrk)用于需要超过 512 字节的对象。
这些分配通过 C 库的 malloc 完成。

1039c000-10449000 rw-p 00000000 00:00 0  [heap]
  >>>>> Found: large string (0x10406840) in this block

4. 匿名内存映射(Python 的 pymalloc)

Python 的私有分配器(pymalloc)会获取称为 arena 的大块匿名内存。
每个 arena 被细分为 pool,用于服务小对象。

>>>> Found: small string (0x7f75e62ec730) in this block
>>>> Found: main function (0x7f75e62d85e0) in this block
>>>> Found: large number (0x7f75e67e3f30) in this block
>>>> Found: sys module (0x7f75e67a2ca0) in this block

想更深入了解 arena、pool 以及整体策略,请参阅:

5. 栈区(函数调用)

栈保存 C 级别的调用帧和局部变量。Python 对象本身永远不会驻留在栈上。

7fff20372000-7fff20394000 rw-p 00000000 00:00 0  [stack]

6. 内核接口区

这些映射([vvar][vdso])由内核用于提供快速系统调用接口。
它们与 Python 的对象分配并无直接关系。

7fff203b8000-7fff203bc000 r--p 00000000 00:00 0  [vvar]
7fff203bc000-7fff203be000 r-xp 00000000 00:00 0  [vdso]

Source:

Python 如何通过引用计数进行垃圾回收

每个 CPython 对象都包含一个引用计数 (ob_refcnt)。当计数降为零时,对象的内存会被回收。

/* CPython’s PyObject structure */
typedef struct _object {
    Py_ssize_t ob_refcnt;    /* reference count */
    PyTypeObject *ob_type;  /* pointer to type object */
    /* ... type‑specific fields follow ... */
} PyObject;

小示例

import sys
import weakref

class Node:
    def __init__(self, name):
        self.name = name
        self.child = None

    def __repr__(self):
        return f"Node({self.name!r})"

# 创建两个节点
a = Node('a')          # refcnt = 1
b = Node('b')          # refcnt = 1

# 弱引用 **不会** 增加引用计数
a_weak = weakref.ref(a)

print('a refcnt:', sys.getrefcount(a))   # 通常为 2(一个来自 getrefcount 参数的临时引用)
print('a weakref alive?', a_weak() is not None)

# 创建引用循环
a.child = b
b.child = a

print('a refcnt after cycle:', sys.getrefcount(a))
print('b refcnt after cycle:', sys.getrefcount(b))

# 打破强引用
del a
del b

# 循环仍然使对象的引用计数大于 0,因而对象仍然存活,
# 但垃圾回收器最终会将它们收集。
import gc
gc.collect()
  • sys.getrefcount() 显示当前的引用计数(调用本身会临时增加一个引用)。
  • 弱引用 (weakref.ref) 允许在不影响计数的情况下引用对象。
  • 当创建引用循环 (a.child = b; b.child = a) 时,引用计数永远达不到零,于是 CPython 的循环垃圾回收器会介入,打破循环并释放内存。

在下一节中我们将深入探讨该循环垃圾回收器的细节。

Source:

引用计数与弱引用

import sys, weakref, gc

class Node:
    child = None

a = Node()
b = Node()
b_ref = weakref.ref(b)

# 创建一个循环
a.child = b
b.child = a          # 引用计数 = 2

print("All refs a, b, and a_ref assigned", f"{sys.getrefcount(a_ref())=}")  
# 2(两个强引用)+ 1 用于传递给 getrefcount 的参数 = 3

del a
print("After variable a deleted", f"{sys.getrefcount(a_ref())=}")   # 引用计数 = 1

del b
print("After both variables deleted", f"{sys.getrefcount(a_ref())=}")  
# 引用计数 = 0,对象已释放 —— 但 `sys.getrefcount` 仍显示 1!
print("After both variables deleted", f"{sys.getrefcount(b_ref())=}")  
# 同样的结果

发生了什么?

即使在删除 ab 之后,弱引用仍然可以取回对象。
添加下面的代码可以查看还有哪些东西在引用 a

import gc   # 垃圾回收器接口模块
print(f"Referrers to a: {gc.get_referrers(a_ref())}")

输出显示了两个引用:

  • a.child → b
  • b.child → a

虽然我们删除了变量 a,但属性 a.child 仍然存在,使得 b 仍然存活,反之亦然。

Python 如何处理这种循环?

Python 使用 标记‑清除(mark‑and‑sweep) 垃圾回收器,其工作方式如下:

  1. 从一组 对象(全局变量、局部变量等)开始。
  2. 递归标记所有可以从这些根对象到达的对象。
  3. 清除所有 未被标记(即不可达)的对象。

如果在每次赋值后都运行该算法,开销将会非常大,因此 CPython 会 周期性 地运行它,依据代(generation)阈值。

import gc
print(f"GC generations thresholds: {gc.get_threshold()}")   # 默认: (700, 10, 10)

代(Generations)

描述收集频率
0新创建且尚未被扫描的对象。700 次分配(默认)
1在第 0 代收集后仍然存活的对象。10 次第 0 代收集后
2在第 1 代收集后仍然存活的对象。10 次第 1 代收集后

这遵循 代际假设:大多数对象“早逝”,而长寿对象往往会一直存活。

因为循环 a ↔ b 仍然位于第 0 代,当我们删除变量时,垃圾回收器尚未运行,所以弱引用仍然能够访问这些对象。

强制进行一次收集

import sys, weakref, gc

class Node:
    child = None

a = Node()
b = Node()
b_ref = weakref.ref(b)

a.child = b
b.child = a

print("All refs a, b, and a_ref assigned", f"{sys.getrefcount(a_ref())=}")

del a
print("After variable a deleted", f"{sys.getrefcount(a_ref())=}")

del b
print("After both variables deleted", f"{sys.getrefcount(b_ref())=}")

# 强制收集第 0、1、2 代
n_collected = gc.collect(2)
print(f"Collected {n_collected} objects")   # → 2 objects

GIL 在垃圾回收中的作用

全局解释器锁(GIL) 使得引用计数的更新是原子操作,从而保证了线程安全的垃圾回收。如果没有 GIL,引用计数的并发修改可能导致竞争条件、内存损坏,进而破坏回收器的工作。


希望你喜欢这次对 CPython 内存管理的深入探讨。理解引用计数、弱引用以及代际垃圾回收器的工作原理,有助于解释在编写 Python 代码时可能遇到的许多细微行为。

0 浏览
Back to Blog

相关文章

阅读更多 »

Python–TypeScript 合约

《The Python–TypeScript Contract》封面图片 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to...