📄 一句话概括
三块地各管一类东西:静态区放类信息和常量、堆放实例、栈放局部变量——引用计数从 2 涨到 4,就是栈上多了两个名字。
📝 本节和前面几节的关系
5.4 讲线程,这一节回到内存本身:程序的地皮是怎么分的。
一、三块地
静态区:模块级常量、类信息(5.7 会看到只有一份)。程序启动时分配,结束时释放。 堆:实例、列表、字典这些对象。new 的时候分配,垃圾回收释放。 栈:局部变量——注意栈上放的是地址,不是物体本身。 三块地的寿命完全不同,这是它们分开的理由。
二、实测
静态区里 STATIC 的 id、堆上 b1/b2 的 id——两个堆地址不一样,说明占了两块。
b1 is b2 是 False:值一样,但不是同一个物体。
引用计数从 2 涨到 4:函数内多了参数 x 和局部 local 两个名字,都在栈上。
函数一返回,栈上那两格消失,计数自动减回去——不需要任何回收动作。
三、为什么不用一块地
寿命不同:栈上的东西函数返回就该消失,堆上的要活到没人用为止。 大小不同:栈很小(几 MB)但要快,堆很大但要管理。 归属不同:栈归函数调用,堆归对象图,静态区归整个程序。 混在一起就只能二选一:要么全快,要么全大。
四、为什么 OOP 要讲这个
因为对象全在堆上,而类信息在静态区——一份类信息,N 块堆对象。 这句话就是 5.7、5.8 的全部内容。 再往后:变量在栈上存地址(5.9)、堆块要回收(5.13)——都是这张三区图的推论。
五、动手 Demo:使用静态区、堆区和栈区进行管理
一个程序把三块地同时演一遍:常量、实例、函数内的局部变量。 数一数它们各在哪块、寿命多长。
import sys
STATIC = "版本 1.0" # 静态区
b1, b2 = Box(9), Box(9) # 堆:两块
def take(x):
local = x # 栈:两个名字
return sys.getrefcount(local)
print(sys.getrefcount(b1)) # 模块级:2
print(take(b1)) # 函数内:4
实跑输出:
--------------------------------------------------------------------
使用静态区、堆区和栈区进行管理
--------------------------------------------------------------------
程序的内存粗分三块。**用一个程序把三块同时演一遍:**
区 放什么 谁分配 谁回收
----------------------------------------------------------------
静态区 模块级常量、类信息 程序启动时 程序结束时
堆 实例、列表、字典等对象 new 的时候 垃圾回收
栈 局部变量(存的是地址) 函数调用时 函数返回时
实测:
静态区里 STATIC 的 id 2349309244832
堆上 b1 的 id 2349308906192
堆上 b2 的 id 2349299158544
模块级看 b1 的引用计数 2
函数内看 local 的引用计数 4 ← 栈上多了两个名字
b1 is b2 False
**注意 b1 和 b2 的值一样、id 不一样**:堆上占了两块,各存一份。
**而 `local` 只是栈上多了一个【指向 b1 的地址】**——引用计数因此从 2 涨到 4。
**函数一返回,栈上那两格就没了,引用计数自动减回去。**
实测:b1/b2 的 id 不同(值一样、物体两个),引用计数从 2 涨到 4。 栈上多两个名字(参数 x、局部 local)就多两票——函数返回后自动减回去,不用回收动作。
30 秒实验
- 写一个函数,在里外各测一次
getrefcount,看差几票。 - 想一想:为什么递归太深会爆栈,而不是爆堆。
常见误区
- 以为栈上放着对象本身。栈上放的是地址。
- 以为三个区是三种内存条。它们是三种管理方式,画在同一块内存里。
自测三问(点开对答案)
1. 三个区各放什么? —— 静态区放模块级常量和类信息;堆放实例、列表、字典;栈放局部变量(存的是地址)。 2. 三块地的寿命各是什么? —— 静态区从程序启动到结束;堆块靠垃圾回收;栈上的东西随函数返回消失。 3. 为什么引用计数会从 2 涨到 4? —— 函数内多了参数 x 和局部 local 两个名字,它们都在栈上,各算一票。- ☐ 下一节(5.6 OOP 的特征在于内存的用法)
相关笔记
- OOP 的特征在于内存的用法 — 数据放在哪块
- 孤立的实例由垃圾回收处理 — 堆块谁回收
- 🔵 计算机基础 · 索引 — 栈与堆的基础
- 🧩 面向对象是怎样工作的 · 索引