当前位置:首页>python>Python 程序退出时,是否释放所有内存分配?

Python 程序退出时,是否释放所有内存分配?

  • 2026-10-11 07:38:41
Python 程序退出时,是否释放所有内存分配?

进程一退出,top 里的 RSS 掉没了。 这时候还纠结 del obj、gc.collect() 有没有跑完,其实方向有点偏。 我一般先分清两件事:Python 有没有优雅清理对象,和 操作系统有没有回收这个进程占过的内存。

这俩不是一回事。

看一段小脚本:

# mem_exit_probe.py
import os
import time

bags = []

defrss_hint():
with open(f"/proc/{os.getpid()}/status") as f:
for line in f:
if line.startswith("VmRSS"):
return line.strip()

for i in range(10):
    bags.append(bytearray(40 * 1024 * 1024))
    print(f"round={i + 1}, {rss_hint()}")
    time.sleep(0.5)

print("pid =", os.getpid())
print("press enter to exit")
input()

另开一个窗口看:

python3 mem_exit_probe.py
watch -n 1 "ps -o pid,rss,cmd -p <pid>"

你会看到进程活着的时候,RSS 一路往上涨。按回车退出,进程没了,RSS 也没了。

所以第一层答案很直接:Python 程序退出后,这个进程的虚拟地址空间会被操作系统回收,普通堆内存、栈内存、mmap 映射,大多数都不会继续挂在系统里。

但这里别急着下结论,说“所有内存都释放了”。这个说法太粗。

Python 退出时,解释器会做一轮收尾,比如跑 atexit,释放一部分模块、对象、缓冲区。正常退出大概是这样:

# graceful_exit.py
import atexit
import os

classTmpHolder:
def__init__(self):
        self.buf = bytearray(100 * 1024 * 1024)

def__del__(self):
        print("__del__ called")

holder = TmpHolder()

@atexit.register
defclose_probe():
    print("atexit called, pid =", os.getpid())

print("normal exit")

执行后大概率能看到:

normal exit
atexit called, pid = 7312
__del__ called

但这地方我不太建议把 __del__ 当成线上资源释放的主力。解释器关闭阶段,模块全局变量可能已经被清掉了,对象之间如果有循环引用,析构顺序也不一定让你舒服。

更狠一点,换成这个:

# hard_exit.py
import atexit
import os

buf = bytearray(200 * 1024 * 1024)

@atexit.register
defbye():
    print("you will not see me")

print("pid =", os.getpid())
os._exit(7)

os._exit() 是直接让进程退出,不走 Python 那套清理流程。atexit 不跑,finally 不跑,很多对象析构也不跑。

但是,进程没了,操作系统仍然会把这个进程的地址空间收掉。 这就是区别:Python 没收尾,不代表内存还归这个进程占着。进程都没了。

再看一个更容易误判的现场。

有时候线上服务内存涨了,代码里明明 del 了,gc.collect() 也打了,RSS 就是不降。然后有人说 Python 内存泄漏。

不一定。

# rss_not_drop.py
import gc
import os
import time

defrss():
with open(f"/proc/{os.getpid()}/status") as f:
return [x for x in f if x.startswith("VmRSS")][0].strip()

rows = []
for i in range(500_000):
    rows.append({
"uid": i,
"name": f"user_{i}",
"tags": ["vip", "paid", "active"]
    })

print("after alloc:", rss())

del rows
gc.collect()

print("after del:", rss())
time.sleep(30)

这里经常会看到一个现象:对象确实没了,但 RSS 没明显下降。

这地方第一眼别骂 GC。CPython 自己有内存分配器,小对象会走一套池化管理。释放对象后,那些内存块可能先留在解释器里复用,不一定马上还给操作系统。再往下还有 glibc 的 malloc 行为,也不保证你释放一块内存,RSS 马上漂亮地掉下去。

所以判断 Python 内存问题,不能只盯 RSS。 我一般会同时看这几个东西:

import gc
import tracemalloc

tracemalloc.start()

# 跑一段业务逻辑
# ...

snapshot = tracemalloc.take_snapshot()
for item in snapshot.statistics("lineno")[:10]:
    print(item)

print("gc objects:", len(gc.get_objects()))

tracemalloc 能看到 Python 层对象主要分配在哪里。 如果 tracemalloc 没涨,RSS 涨得很凶,那我会开始怀疑:是不是 native 扩展、numpy、pandas、图片库、压缩库、驱动层在吃内存。

比如这种就不是普通 Python 对象:

# native_alloc_linux.py
import ctypes
import os
import time

libc = ctypes.CDLL(None)
libc.malloc.restype = ctypes.c_void_p
libc.memset.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_size_t]

ptrs = []

for i in range(8):
    p = libc.malloc(50 * 1024 * 1024)
ifnot p:
raise MemoryError("malloc failed")

    libc.memset(p, 1, 50 * 1024 * 1024)
    ptrs.append(p)

    print("native block:", i + 1, "pid:", os.getpid())
    time.sleep(1)

print("not calling free, press enter to exit")
input()

这段代码故意不 free。进程活着的时候,这就是泄漏。 但进程退出后,操作系统还是会把这部分内存收掉。

这也是为什么很多批处理脚本偷懒能活得挺好:跑一次,吃一坨内存,跑完进程退出,系统擦屁股。 但 Web 服务、消费者、定时常驻进程不行。它们不退出,泄漏就一直攒,最后 OOM。

还有一类别混进去:进程退出后,不是所有“资源”都会消失。

普通内存会回收,但这些东西要单独看:

# file_left.py
from pathlib import Path

p = Path("/tmp/import_task.lock")
p.write_text("running")
raise RuntimeError("boom")

程序崩了,/tmp/import_task.lock 还在。 这不是内存,但线上事故里经常被一起算成“资源没释放”。

类似的还有临时文件、数据库里没提交完的业务状态、外部服务里的会话、消息队列里未 ack 的消息、你手动创建的共享内存名字。操作系统能回收进程自己的内存,不代表能替你把业务现场也擦干净。

我平时写这类脚本,会把关键资源放进上下文管理器里,别赌退出时析构:

from pathlib import Path
import tempfile

defexport_user_snapshot(user_ids):
    tmp_dir = Path(tempfile.mkdtemp(prefix="user_export_"))
    marker = tmp_dir / "RUNNING"

    marker.write_text("1")

try:
        out = tmp_dir / "users.csv"
with out.open("w", encoding="utf-8") as f:
            f.write("uid,status\n")
for uid in user_ids:
                f.write(f"{uid},ok\n")

        marker.rename(tmp_dir / "DONE")
return out

except Exception:
        marker.rename(tmp_dir / "FAILED")
raise

这里不是为了“释放内存”,而是为了让程序异常退出时,现场能看懂。线上排障时,一个 FAILED 文件比一堆猜测管用。

回到问题本身。

Python 程序退出时,操作系统会回收这个进程占用的内存。正常退出时,Python 解释器还会尽量做一轮对象清理。可如果是 os._exit()、SIGKILL、容器被强杀,Python 层的清理逻辑可能根本没机会执行。

所以写代码时别把“进程退出会释放内存”当成免死金牌。

短命脚本,可以接受退出兜底。 常驻服务,内存涨了就要查引用、查缓存、查 native 扩展。 外部资源,该 close 就 close,该 with 就 with,该落状态就落状态。

内存最后会被系统收走。 但线上留下的烂摊子,不一定。

最新文章

随机文章