🧩 第五章 理解内存结构:程序员的基本素养
📄 一句话概括

三块地各管一类东西:静态区放类信息和常量、堆放实例、栈放局部变量——引用计数从 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 秒实验

  1. 写一个函数,在里外各测一次 getrefcount,看差几票。
  2. 想一想:为什么递归太深会爆栈,而不是爆堆。

常见误区

  1. 以为栈上放着对象本身。栈上放的是地址。
  2. 以为三个区是三种内存条。它们是三种管理方式,画在同一块内存里。
自测三问(点开对答案) 1. 三个区各放什么? —— 静态区放模块级常量和类信息;堆放实例、列表、字典;栈放局部变量(存的是地址)。 2. 三块地的寿命各是什么? —— 静态区从程序启动到结束;堆块靠垃圾回收;栈上的东西随函数返回消失。 3. 为什么引用计数会从 2 涨到 4? —— 函数内多了参数 x 和局部 local 两个名字,它们都在栈上,各算一票。

相关笔记