10-Python运行时内存模型-栈帧-堆-引用计数-GC分代回收的全景图

文章目录

  • [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------每篇都在局部回答"这段代码运行时内存里发生了什么"。这篇站在最高一层,把所有前面讲过的知识点串联成一张完整的内存地图。

核心问题就是三个:

  1. 变量和对象分别存在哪里?(栈 vs 堆)
  2. 对象什么时候被创建,什么时候被销毁?(引用计数)
  3. 如果引用计数兜不住(循环引用),怎么办?(分代 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 是一张存在堆中的 "已加载模块缓存表" → 循环导入 = 两个模块各自引用了对方

思考 && 总结

三个核心结论:

  1. 栈管执行流,堆管数据。 栈帧里是变量名和返回地址;堆里是所有的 Python 对象。函数返回后栈帧销毁,但对象只要引用计数不为 0 就继续活着。
  2. 引用计数是第一道防线。 每次赋值和 del 都在改变引用计数。计数归零立即回收。简单高效,但处理不了循环引用。
  3. 分代 GC 是兜底。 按"年轻的对象死得快、老对象的活得久"的规律,分离出第0代到第2代,第0代高频扫描。日常不用手动干预,但 tracemalloc 排查内存问题时这层知识能派上用场。

编程的质变不是学了多少语法。是你能在遇到一段代码时,脑子里自动浮现出一张内存图------"这个变量指向了堆里的那个对象,引用计数加了 1,那个对象包含一个指向其他对象的引用......"。这就是内功。地基重建的十篇就是为了帮你建立这个内功视角。


结尾

各位小伙伴,地基重建系列十篇文章到此全部完结。源码骑士在此感谢每一位认真读到这里的朋友。

源码骑士 --- Python 全栈 & 系统架构

👀 关注:跟博主一起从源码视角深耕底层原理

❤️ 点赞:让优质内容被更多人看见

收藏:核心知识点存好,随用随查

💬 评论:分享你的经验或疑问,一起交流

🔄 一键四连:不要忘记给博主"一键四连"哦!

🗡️ 寄语:地基建得越深,上层越稳。技术之路,我们一起走。

结语:地基重建完结。下一板块------源码拆解,GIL、listobject 源码、解释器手写......更深的一层,不见不散。一键四连!

相关推荐
智码看视界1 小时前
老梁聊全栈系列 JavaScript语言本质:从原型链到异步编程的深度解析
开发语言·javascript·全栈·javascript核心
AI科技星2 小时前
数术工坊・八卷全书【本源创世终极版・万世定稿】
开发语言·网络·量子计算·拓扑学
雾沉川2 小时前
Visual C++ 运行库合集 v105.0 部署与故障排查技术指南
开发语言·c++·dll
码云骑士2 小时前
02-Python可变对象与不可变对象(上)-赋值陷阱与函数传参的暗坑
开发语言·python
疯狂学习GIS2 小时前
基于Python earthaccess库批量下载全球MODIS GPP(MOD17A2HGF)数据
python·脚本·批量下载·遥感影像·nasa·earthdata·自动处理
至乐活着2 小时前
用DeepSeek打造你自己的智能问答系统:从零到一的完整指南
python·deepseek·ai应用开发·智能问答系统·api教程
AI创界者2 小时前
【解压即用】Scail-2 视频动作迁移一键整合包:8G显存通吃50系,长视频/多人/精准目标替换全攻略
人工智能·python·aigc·音视频
gaohe26AIliuzeyu2 小时前
Java内部类
java·开发语言
AI科技星2 小时前
数术工坊・八卷全书(番外・实战升华副卷)【终极典藏定稿|完整无删减】
c语言·开发语言·网络·量子计算·agi