文章目录
- [Python 运行时内存模型:一张图画完栈帧、堆、引用计数与GC分代回收](#Python 运行时内存模型:一张图画完栈帧、堆、引用计数与GC分代回收)
-
- 导入语
- [1 ~> 栈(Stack)与堆(Heap)------Python 内存的两大分区](#1 ~> 栈(Stack)与堆(Heap)——Python 内存的两大分区)
-
- [1.1 一张图画完](#1.1 一张图画完)
- [1.2 栈帧](#1.2 栈帧)
- [1.3 堆](#1.3 堆)
- [2 ~> 引用计数------对象的第一道生死线](#2 ~> 引用计数——对象的第一道生死线)
-
- [2.1 每个对象有个计数器](#2.1 每个对象有个计数器)
- [2.2 引用计数的变化过程](#2.2 引用计数的变化过程)
- [2.3 引用计数与可变对象的关联(回顾)](#2.3 引用计数与可变对象的关联(回顾))
- [3 ~> 分代 GC------引用计数的兜底机制](#3 ~> 分代 GC——引用计数的兜底机制)
-
- [3.1 引用计数解决不了的问题](#3.1 引用计数解决不了的问题)
- [3.2 分代回收是怎么工作的](#3.2 分代回收是怎么工作的)
- [3.3 可以用 `gc` 模块手动干预](#3.3 可以用
gc模块手动干预)
- [4 ~> 知识地图:前面九篇分别对应这张图哪里](#4 ~> 知识地图:前面九篇分别对应这张图哪里)
- [思考 && 总结](#思考 && 总结)
- 结尾
Python 运行时内存模型:一张图画完栈帧、堆、引用计数与GC分代回收
📖 文章简介: 第一板块的收官总结篇。在前面九篇文章拆解了变量赋值、对象可变性、深浅拷贝、迭代器、字典hash表、装饰器、异常处理和import机制之后,本篇站在更高维度把所有知识点串成一张 Python 运行时内存全景图。核心内容:栈帧与堆的划分(局部变量存在栈帧、对象存在堆里)、引用计数如何决定对象的生死、分代垃圾回收(Generational GC)如何处理循环引用、以及 gc 模块的基本用法。配一张完整的运行时内存布局图,把前面九篇的知识点全部标注在对应位置,让读者获得 Python 内存模型的"上帝视角"。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
这是地基重建系列的最后一篇。前面九篇分别讲了变量、对象、深浅拷贝、迭代器、字典、装饰器、异常、import------每篇都在局部回答"这段代码运行时内存里发生了什么"。这篇站在最高一层,把所有前面讲过的知识点串联成一张完整的内存地图。
核心问题就是三个:
- 变量和对象分别存在哪里?(栈 vs 堆)
- 对象什么时候被创建,什么时候被销毁?(引用计数)
- 如果引用计数兜不住(循环引用),怎么办?(分代 GC)
搞清这三个问题,前面九篇的所有知识点自然归位。
1 ~> 栈(Stack)与堆(Heap)------Python 内存的两大分区
1.1 一张图画完
bash
┌──────────────────────────────────────────────────────────────┐
│ Python 进程内存空间 │
│ │
│ ┌───────────────────────┐ ┌─────────────────────────────┐ │
│ │ 调用栈(Stack) │ │ 堆(Heap) │ │
│ │ │ │ │ │
│ │ main() 的栈帧 │ │ int对象(1) ←─ 小整数池 │ │
│ │ ├─ a →────────────┐ │ │ int对象(300) │ │
│ │ ├─ b →──────────┐ │ │ │ list对象([1,2,3]) ← a │ │
│ │ ├─ lst →────────┤ │ │ │ str对象("hello") ← b │ │
│ │ └─ d →──────────┤ │ │ │ dict对象({"key":"val"}) │ │
│ │ │ │ │ │ │ │
│ │ helper() 的栈帧 │ │ │ │ [GC 分代] │ │
│ │ ├─ x →───────────┼─┼───┼──→ int对象(4) │ │
│ │ └─ result →──────┼─┼───┼──→ int对象(10) ←─ 新分配 │ │
│ └───────────────────┘ │ └─────────────────────────────┘ │
│ │ │
│ 自由变量存储区(cells) │ [引用计数表] │
│ ┌──────────────────┐ │ [1,2,3] 引用计数=2 (a,lst) │
│ │ times_3.__closure│←──┘ "hello" 引用计数=1 (b) │
│ │ [cell: n=3] │ 300 引用计数=0 → 待回收 │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘
1.2 栈帧
每个函数调用都会创建一个栈帧,压在调用栈上。 栈帧里存放的是:
- 该函数的局部变量(变量名 → 对象引用的映射)
- 返回地址(函数执行完回到哪里)
- 临时计算结果
python
def foo():
x = 10 # x 这个变量名存在 foo 的栈帧里,它引用的是堆中的 int 对象 10
y = [1, 2] # y 这个变量名存在 foo 的栈帧里,它引用的是堆中的 list 对象
函数返回时,栈帧被销毁(从栈中弹出),该函数内的局部变量名不再存在。但对象是否销毁不取决于栈帧------取决于引用计数。
1.3 堆
所有 Python 对象(int、list、dict、函数、类......)全部存在堆中。 堆由 Python 内存管理器统一管理。
bash
栈 = 函数执行的"纸片"──很快就销毁
堆 = 能长期存活的"仓库"──一块能跨函数一直活到没人引用为止的空间
2 ~> 引用计数------对象的第一道生死线
2.1 每个对象有个计数器
Python 在每个对象内部维护一个引用计数。每当有变量指向它,计数 +1;变量不再指向它(被覆盖 / del / 离开作用域),计数 -1。计数归零 → 对象被立即回收。
2.2 引用计数的变化过程
python
import sys
a = [1, 2, 3] # 对象 [1,2,3] 引用计数 = 1
print(sys.getrefcount(a)) # 输出 2(a一个,getrefcount的参数临时多一个)
b = a # 引用计数 +1 → 现在是 2
c = [a, a] # 引用计数 +2 → 现在是 4
del b # 引用计数 -1 → 3
c.clear() # 引用计数 -2 → 1
del a # 引用计数 -1 → 0 → 回收
2.3 引用计数与可变对象的关联(回顾)
引用计数也能解释之前讲过的现象。你看到 b = a 时 a 和 b 共享同一个 list,因为引用计数是从 1 变到了 2------对象并没有被复制,只是标签变多了。这也是"浅拷贝 vs 深拷贝"的根源:浅拷贝不复制嵌套对象------靠引用计数撑腰。
3 ~> 分代 GC------引用计数的兜底机制
3.1 引用计数解决不了的问题
python
class Node:
def __init__(self, name):
self.name = name
self.ref = None
a = Node("A")
b = Node("B")
a.ref = b
b.ref = a # a 和 b 互相引用
del a
del b # 两个对象的引用计数都不是 0
# 但因为互指关系,这两个 Node 还活着,占着内存
CPython 之所以能回收它们,靠的是分代垃圾回收器。
3.2 分代回收是怎么工作的
Python 把对象分为三代:第0代(新对象)、第1代(活过一轮 GC 的)、第2代(最老的对象)。
- 第0代垃圾回收的频率最高,最老的一代最少被扫描
- 每次 GC 扫描第0代,如果发现"该回收的对象被循环引用卡住了",就判定它们存活了------将其提升到第1代
- 如果一个对象被引用计数所释放,GC 根本不会管它------GC 只在引用计数解决不了的区域里工作
3.3 可以用 gc 模块手动干预
python
import gc
gc.collect() # 手动触发一次 GC
print(gc.get_count()) # 查看各代当前对象数
print(gc.get_threshold()) # 查看各代触发 GC 的阈值
gc.set_threshold(700, 10, 10) # 手动修改阈值
日常不需要显式调 gc.collect(),Python 自动处理得很好。只有在处理大量短生命周期、互相引用的对象(比如图结构)的批处理中手动清理才有价值。
4 ~> 知识地图:前面九篇分别对应这张图哪里
bash
文章01 --- Python变量赋值的底层真相
→ 栈中的变量名 vs 堆中的对象 ← 变量是指向堆中对象的"标签"
文章02~03 --- 可变对象、不可变对象、深浅拷贝
→ 引用计数的增减 → 同一个对象共享 vs 复制 → 浅拷只复制一层、深拷递归复制
文章04 --- 循环中删除元素
→ 迭代器持有列表对象的引用 → 引用计数不为0 → 遍历中删除改变了容器结构
文章05 --- 字典的 Hash 表
→ 字典对象在堆中的存储:索引表 + 数据表 → 开放寻址法处理哈希碰撞
文章06~07 --- 装饰器、闭包
→ 函数是一等公民,函数对象也存在堆中 → __closure__ 中的 cell 存储自由变量
文章08 --- 异常处理
→ try/except/finally 各自对应的栈帧操作 → finally 在函数真正返回前执行
文章09 --- import 机制
→ sys.modules 是一张存在堆中的 "已加载模块缓存表" → 循环导入 = 两个模块各自引用了对方
思考 && 总结
三个核心结论:
- 栈管执行流,堆管数据。 栈帧里是变量名和返回地址;堆里是所有的 Python 对象。函数返回后栈帧销毁,但对象只要引用计数不为 0 就继续活着。
- 引用计数是第一道防线。 每次赋值和
del都在改变引用计数。计数归零立即回收。简单高效,但处理不了循环引用。 - 分代 GC 是兜底。 按"年轻的对象死得快、老对象的活得久"的规律,分离出第0代到第2代,第0代高频扫描。日常不用手动干预,但
tracemalloc排查内存问题时这层知识能派上用场。
编程的质变不是学了多少语法。是你能在遇到一段代码时,脑子里自动浮现出一张内存图------"这个变量指向了堆里的那个对象,引用计数加了 1,那个对象包含一个指向其他对象的引用......"。这就是内功。地基重建的十篇就是为了帮你建立这个内功视角。
结尾
各位小伙伴,地基重建系列十篇文章到此全部完结。源码骑士在此感谢每一位认真读到这里的朋友。
源码骑士 --- Python 全栈 & 系统架构
👀 关注:跟博主一起从源码视角深耕底层原理
❤️ 点赞:让优质内容被更多人看见
⭐ 收藏:核心知识点存好,随用随查
💬 评论:分享你的经验或疑问,一起交流
🔄 一键四连:不要忘记给博主"一键四连"哦!
🗡️ 寄语:地基建得越深,上层越稳。技术之路,我们一起走。
结语:地基重建完结。下一板块------源码拆解,GIL、listobject 源码、解释器手写......更深的一层,不见不散。一键四连!