Python 程序退出时,是否释放所有内存分配?
Python 进程都退出了,内存还不释放?
这个问题我一般不会先看 Python 代码。先看进程还在不在。
有一次线上跑批任务,日志里明明已经打印了:
[archive-job] done, cost=38s
[archive-job] exit
但机器内存还没下来。业务同学第一反应是:Python 是不是内存泄漏了?我第一眼就不太信。进程退出以后,普通堆内存还不还,主要不是 Python 说了算,是操作系统说了算。
先把话放这:Python 程序正常退出后,进程占用的大部分内存都会被操作系统回收。
但这句话别理解满了。
Python 里的对象释放,和进程退出时操作系统回收内存,是两回事。
看个小脚本:
# mem_burst.py
import os
import timedefrss_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 timeshm = 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_memorydefclear_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 timecache = []
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 multiprocessingprint("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 weakrefclassBatchBox:
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 Pathdefexport_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 堆内存,最后再查引用。顺序错了,半天都在跟假问题较劲。