⚙️ 第八章 从源文件到可执行文件
📄 一句话概括

栈自动且浅(函数调用、局部变量,太深就爆栈);堆手动且大(动态申请,漏了就一天天变慢)。

📝 本节和前面几节的关系

8.7 说了运行需要什么,这一节看程序真正跑起来之后,手里握着哪两块地方。

一、栈:自动的、很浅

栈用来放函数调用和局部变量。 它的分配是自动的:进函数就压一层,出函数就弹掉。 所以它极快——所谓分配,不过是把指针挪一下。 但它有一个硬伤:很浅。 栈的大小通常固定,递归太深就撑不住。

二、堆:手动申请的、可以很大

堆用来放那些【大小事先不确定】的数据。 它的分配是手动的:你申请,你释放。 所以它较慢——得先去找一块够大的地方,还要处理碎片。 好处是可以长得很大,唯一的限制只剩物理内存。

三、实测两件事

爆栈:把递归调到上限的三倍,立刻报 RecursionError。 实测:递归上限 1000,退到 900 层还顺利;再深 3 倍就直接炸了。 堆增长:造 10 万个对象,堆上就多出 100001 个;删掉再回收,干净回到基准。 两个实验各说明一件事:栈是有限的、会自动收回;堆是弹性的、要你负责收回。

四、内存泄漏为什么可怕

栈爆了,程序立刻崩——这是个痛快的结果,好定位也好修。 堆漏了却相反:程序照跑,只是内存一天天涨上去。 最后的表现往往是【跑几天就变慢、重启一下就好】——这类症状最难查。 所以老话说得对:栈的问题当场报,堆的问题拖到你忘了它。

五、动手 Demo:加载时生成的栈和堆

程序运行时有两块重要的地方:栈和堆。 栈自动、浅;堆手动、大。各实测一次。

import sys, gc
limit = sys.getrecursionlimit()
def rec(n):
    if n <= 0:
        return 0
    return rec(n - 1) + 1
print("递归上限", limit, "退", limit - 100, "层 ->", rec(limit - 100))
gc.collect()
n0 = len(gc.get_objects())
big = []
for i in range(100000):
    big.append([i])
print("堆上多了", len(gc.get_objects()) - n0, "个对象")

实跑输出:

--------------------------------------------------------------------
加载时生成的栈和堆
--------------------------------------------------------------------

程序运行时有两块重要的地方:**栈和堆**。

                 栈                                   堆
  ---------------------------------------------------------------------------
  什么时候用     函数调用、局部变量                   需要动态申请的内存
  怎么分配       **自动**,进函数就压、出函数就弹     **手动**申请与释放
  快不快         极快(只是挪一下指针)               较慢(要找一块够大的)
  大小           通常固定,**太深就爆栈**             可以长得很大

  先实测【爆栈】——**栈是有上限的,递归太深就撑不住:**

    当前递归上限    1000
    退到 900 层      900(顺利返回)
    再深 3 倍       **RecursionError:栈不够用了**

  再实测【堆】——**每造一个对象,堆上就多一份:**

    造之前        基准
    造 10 万个对象  堆上多了 100001 个对象
    删掉再回收      干净回到基准

  **两句话记住:栈是自动的、很浅;堆是手动申请的、可以很大。**
  栈爆了程序立刻崩;**堆漏了则是一天天变慢——这就是内存泄漏。**

递归上限 1000,退到 900 层还顺利;再深 3 倍就 RecursionError——栈不够用。 造 10 万个对象,堆上就多 100001 个;删掉再回收,干净回到基准。

30 秒实验

  1. 把递归的深度上限调小,观察程序从哪一层开始报错。
  2. 找一段长期运行的程序,看它的内存占用是平的还是持续上升。

常见误区

  1. 以为栈也能无限增长。它通常固定,太深就爆栈。
  2. 以为内存泄漏会立刻崩溃。它表现成【跑久了变慢】,最难排查。
自测三问(点开对答案) 1. 栈和堆各用来放什么? —— 栈放函数调用与局部变量;堆放大小事先不确定、需要动态申请的数据。 2. 为什么栈快、堆慢? —— 栈只是挪一下指针;堆要先找一块够大的空间,还要处理碎片。 3. 爆栈和内存泄漏的表现有什么不同? —— 爆栈立刻崩,好定位;内存泄漏让程序一天天变慢,最难排查。

相关笔记