Python技术迷

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

Python 进程都退出了,内存还不释放?

这个问题我一般不会先看 Python 代码。先看进程还在不在。

有一次线上跑批任务,日志里明明已经打印了:

[archive-job] done, cost=38s
[archive-job] exit

但机器内存还没下来。业务同学第一反应是:Python 是不是内存泄漏了?我第一眼就不太信。进程退出以后,普通堆内存还不还,主要不是 Python 说了算,是操作系统说了算。

先把话放这:Python 程序正常退出后,进程占用的大部分内存都会被操作系统回收。

但这句话别理解满了。

Python 里的对象释放,和进程退出时操作系统回收内存,是两回事。

看个小脚本:

# mem_burst.py
import os
import time

defrss_mb():
with open(f"/proc/{os.getpid()}/status", "r", encoding="utf-8") as f:
for line in f:
if line.startswith("VmRSS:"):
return int(line.split()[1]) // 1024

blocks = []
for i in range(8):
    blocks.append(bytearray(80 * 1024 * 1024))
    print("step", i, "rss =", rss_mb(), "MB")
    time.sleep(0.3)

print("pid =", os.getpid(), "ready to exit")

跑的时候 RSS 会一路涨。脚本结束以后,这个进程没了,RSS 自然也没了。

所以如果你是在命令行里执行一个短命 Python 脚本,里面申请了几百 MB,执行完退出,内存还挂着不动,那我第一步不会去翻 del,也不会去手动 gc.collect()。我会先敲:

ps -ef | grep mem_burst
free -m
top

很多时候你看到的“没释放”,其实是 Linux 把释放出来的内存拿去做 page cache 了。free 里的 available 才更值得看,别盯着 used 自己吓自己。

但是,Python 进程退出不等于所有东西都“干净”。

比如下面这种,内存不是单纯的 Python 对象:

# shm_writer.py
from multiprocessing import shared_memory
import time

shm = shared_memory.SharedMemory(create=True, size=128 * 1024 * 1024)
print("shm name:", shm.name)

shm.buf[:4] = b"dong"
time.sleep(2)

# 这里只 close,不 unlink
shm.close()

这段代码退出以后,共享内存对象可能还留在系统里。它不跟普通 Python list、dict 一样,进程没了就完事。

你得明确清理:

from multiprocessing import shared_memory

defclear_old_segment(name: str):
try:
        seg = shared_memory.SharedMemory(name=name)
except FileNotFoundError:
        print("segment not found:", name)
return

try:
        seg.close()
        seg.unlink()
        print("segment removed:", name)
except FileNotFoundError:
pass

这类东西我一般会单独列清单:共享内存、mmap 文件、临时文件、子进程、socket 连接、GPU 显存、C 扩展库里申请的 native memory。它们不一定跟着 Python 对象析构老老实实走。

还有一种更常见,程序其实没退出,只是主线程退出了,后面还有非 daemon 线程、子进程、任务队列在撑着。

# worker_left.py
import threading
import time

cache = []

defload_forever():
whileTrue:
        cache.append(bytearray(10 * 1024 * 1024))
        time.sleep(1)

t = threading.Thread(target=load_forever)
t.start()

print("main finished")

这段打印完 main finished,进程不会结束。你以为程序退出了,其实它还活着,还在吃内存。

我排这种问题,一般先看线程和子进程,不急着怀疑解释器:

import os
import threading
import multiprocessing

print("pid:", os.getpid())
print("threads:", [t.name for t in threading.enumerate()])
print("children:", multiprocessing.active_children())

如果是服务常驻进程,比如 Flask、FastAPI、Celery worker,那就更不能拿“程序退出会释放内存”来安慰自己。服务没退出,内存当然不会还给操作系统。CPython 会复用自己的内存池,小对象释放后也可能先留在解释器内部,方便下次再用。表现就是:业务对象没了,但 RSS 不一定马上降。

这种场景才需要看对象引用是不是断了。

我会先写一个很土的检查点:

import gc
import weakref

classBatchBox:
def__init__(self, rows):
        self.rows = rows

box = BatchBox([{"sku": i, "payload": "x" * 1000} for i in range(200000)])
watch = weakref.ref(box)

print("before:", watch() isnotNone)

box = None
gc.collect()

print("after:", watch() isnotNone)

如果 after 还是 True,说明还有地方拿着引用。这个时候再去查全局缓存、闭包、lru_cache、队列里没消费完的数据,比较靠谱。

真正要在退出时做清理,可以用 try/finally,别全指望析构函数。

import tempfile
from pathlib import Path

defexport_report(rows):
    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
    tmp_path = Path(tmp.name)

try:
with tmp:
for row in rows:
                tmp.write(f"{row['id']},{row['amount']}\n".encode())

# 假装这里上传文件
        print("upload:", tmp_path)
finally:
if tmp_path.exists():
            tmp_path.unlink()
            print("tmp removed:", tmp_path)

__del__ 我是不太爱用的。退出阶段模块可能已经被卸载一半了,日志对象、连接对象、全局变量的状态都不一定正常。你指望它兜底,线上偶尔就给你来一下玄学。

所以这个问题最后要拆开看。

普通 Python 对象申请的内存,进程退出后操作系统会回收。

Python 进程没退出,只是主逻辑跑完了,那内存不会凭空消失。

共享内存、临时文件、外部资源、子进程、GPU 显存这些东西,不能简单一句“进程退出就释放”糊过去。

常驻服务里的内存上涨,也别拿脚本退出那套逻辑解释。服务不重启,引用还在,缓存还在,内存池还在。该查引用链查引用链,该限制缓存限制缓存,该分批处理分批处理。

我比较烦一种写法:

data = load_all()
handle(data)
del data

好像加个 del 就安全了。多数时候这只是删了一个名字,不代表底层内存马上回到系统。对象还有别的引用,删了也白删;对象确实没引用了,Python 也可能先留着复用。

排内存问题别上来就念口诀。先确认进程是否退出,再确认资源是不是 Python 堆内存,最后再查引用。顺序错了,半天都在跟假问题较劲。