python资源释放问题
这里要讲的,本质上是在自问自答
尽管如此,深挖后发现仍然有些python深层的运行机制需要注意。一起来踏上学习之旅吧~
第一部分 资源释放
问题:python在shell中运行,shell窗口关闭时,python中的变量和资源都会释放吗?
简短回答:是的,几乎都会。 这个问题它触及了操作系统进程管理的核心概念。当Shell窗口关闭时,操作系统会终止在该Shell中运行的所有进程(包括Python解释器),进程所占用的所有资源都会被操作系统强制回收。
一、深入理解:为什么会释放?
1. 进程与资源的关系
当你运行 python3 script.py
时,Shell会创建一个新的进程来执行Python解释器。
这个进程拥有自己独立的内存空间、文件描述符、环境变量等资源。
操作系统内核负责跟踪和管理所有这些资源。
2. Shell关闭时发生了什么?
当你关闭Shell终端(无论是点击“X”、输入 exit
还是断开SSH会话),操作系统会向该Shell进程发送一个 SIGHUP
(Signal Hang Up) 信号。
Shell进程在收到 SIGHUP
后,在退出之前,它会向由它启动的所有子进程(包括你的Python程序)也发送 SIGHUP
信号。默认情况下,SIGHUP
信号的处理方式是终止进程。
3. 进程终止后的清理
当一个进程被终止(无论是自愿退出还是被信号杀死),操作系统内核会自动负责清理该进程所占用的所有资源:
内存:包括堆(Heap)和栈(Stack)中的所有数据,所有Python变量(整数、列表、字典、对象等)所占用的内存都会被彻底释放。
文件描述符:所有打开的文件都会被关闭。
网络连接:所有的Socket连接会被关闭。
其他系统资源:如管道、共享内存段等都会被清理。
可以这样理解: 资源是向操作系统“借”的。进程是“借款人”。借款人消失了(进程终止),操作系统这个“银行”就会自动收回所有借出的资源。
二、例外情况与注意事项
虽然绝大多数资源都会被释放,但在某些极端情况下,可能会留下“垃圾”,不过这非常罕见。
1. 僵尸进程
如果Python进程创建了子进程,并且在Shell终止时,Python父进程先于其子进程被杀死,这些子进程可能会短暂地变成“僵尸进程”。
但是,当Shell退出后,这些僵尸进程的父进程ID (PPID) 会变为 1
(即 init
或 systemd
进程),init
进程会定期“收割”僵尸进程,最终它们也会被完全清理掉。这是一个自动的过程。
2. 外部持久化资源
Python进程管理的内部资源会被释放,但如果你的程序修改了外部资源,这些更改是持久的,不会被回滚:
已写入文件的数据:如果程序已经执行了
file.write()
并且数据已写入磁盘,文件内容会被保留。已发送的网络请求:如果请求已经到达服务器,服务器已经处理,那么这个效果是永久的。
修改了的数据库记录:如果已经执行了
COMMIT
,数据库的更改是永久的。
示例:
# 假设这个程序运行到一半被强制终止
import jsondata = {"name": "test"} # 这个变量在内存中,会被释放
with open("data.json", "w") as f:json.dump(data, f) # 如果执行到这里,数据已经写入磁盘# 如果在这之后进程被杀死,文件 "data.json" 仍然存在且包含数据f.flush() # 强制将缓冲区数据写入磁盘
三、如何模拟和验证?
你可以写一个简单的Python脚本来验证这个过程:
创建测试脚本 test.py
:
#!/usr/bin/env python3
import time
import signal
import atexit# 定义一个很大的列表,占用显著的内存
large_list = [i for i in range(10_000_000)] # 约 80 MB 内存
print(f"Created a large list with {len(large_list)} elements.")# 注册一个退出函数(如果正常退出会被执行)
@atexit.register
def cleanup():print("Cleanup function called!") # 如果这行没打印,说明进程是被强制杀死的# 忽略SIGHUP信号,看看会发生什么
# signal.signal(signal.SIGHUP, signal.SIG_IGN)print("Script is running. Now close the terminal window...")
try:while True:time.sleep(1)
except KeyboardInterrupt:print("Received interrupt.")
验证步骤:
在终端运行
python3 test.py
。直接关闭终端窗口。
重新打开一个终端,运行
htop
或top
命令。观察进程列表,你会发现之前的Python进程已经完全消失了,它占用的内存也被回收了。同时,
Cleanup function called!
这行字也不会出现,因为atexit
函数只在程序正常退出时调用,而强制关闭属于非正常退出。
四、如何避免资源被释放?
如果你希望关闭Shell窗口后Python程序继续运行,你需要让进程与Shell脱离关系,使其不受 SIGHUP
信号影响。有几种方法:
1. nohup
:nohup
会忽略 SIGHUP
信号,&
让进程在后台运行。
2. disown
:disown
命令可以将作业从Shell的作业表中移除,使其不再接收Shell发出的信号。
总结
场景 | 结果 |
---|---|
直接关闭Shell窗口 | 进程被强制终止,所有内存变量和系统资源被操作系统100%回收。 |
程序已修改外部资源 | 修改(文件、数据库)会被保留,因为更改已经发生在进程之外。 |
使用 nohup 、tmux 等方法 | Python进程与Shell分离,继续运行,资源不会被释放。 |
因此,可以放心地关闭Shell窗口,操作系统会为你做好清理工作,不会因为Python变量没有手动释放而导致内存泄漏。
如释重负~~
第二部分 python中类的__del__函数
问题:python调用了一个类,类中写了__del__函数进行了一系列处理。在关闭shell时,__del__函数会执行吗?
简短回答:不会,或者更准确地说,不保证会执行。
当Shell窗口关闭时,操作系统会强制终止Python进程,这是一种“粗暴”的清理方式,不会给Python解释器任何机会去优雅地执行清理代码,包括 __del__
方法。
一、__del__
何时不会执行?
优雅终止:当Python程序正常结束时(例如,代码执行完毕、遇到 sys.exit()
或未捕获的异常),解释器会开始垃圾回收过程。在这个过程中,如果对象的引用计数降为0,它的 __del__
方法就会被调用。
强制终止:当Shell窗口关闭时,操作系统发送 SIGHUP
或 SIGKILL
信号。SIGKILL
(信号9)是尤其“无情”的,它会立即从内核层面彻底移除进程,不会给进程任何机会去执行自己的清理代码(包括 __del__
、atexit
、try/finally
等)。
__del__
方法是一个析构函数 ,它的调用依赖于Python解释器的垃圾回收机制。而操作系统的强制杀死行为,发生在解释器层面之下,完全绕过了Python的垃圾回收流程。想象一下,你的房子(进程)着火了。
优雅终止:你有时间(__del__
)去抢救重要物品、关闭煤气闸(finally
)、然后从大门离开(sys.exit
)。
强制终止:房子被一颗导弹直接击中(SIGKILL
),瞬间化为灰烬。你根本没有时间做任何事。
二、__del__
的正确用途
用途:既然 __del__
不可靠,那它有什么用?
作为一种最后的保障,在对象被Python解释器正常垃圾回收时,释放一些非外部资源。
例如:提醒开发者这个对象应该被更显式地清理了。(在实际开发中,这很少见)。
陷阱:
执行顺序不确定:Python不保证垃圾回收的顺序。如果
__del__
试图访问另一个已经被销毁的对象的属性,会导致难以调试的错误。循环引用问题:如果两个对象有
__del__
方法且相互引用,垃圾回收器可能永远无法回收它们,导致内存泄漏。不保证执行:正如你所经历的,在强制终止、解释器崩溃等情况下,它不会运行。
既然 __del__
不可靠,我们应该使用其他方法来确保资源被正确释放。
三、可靠的资源清理方案
1. 上下文管理器 (with
语句)
这是Python中最推荐的方式。它保证了无论代码块如何退出(即使是异常),__exit__
方法都会被调用。
class ResourceHandler:def __init__(self, name):self.name = namedef __enter__(self):print(f"Acquiring {self.name}")return self # 返回的资源对象def __exit__(self, exc_type, exc_val, exc_tb):# 这里的清理代码无论如何都会执行print(f"Releasing {self.name} (guaranteed!)")with open("release_log.txt", "a") as f:f.write(f"{self.name} was released\n")# 使用方式
with ResourceHandler("DatabaseConnection") as resource:# 使用资源print("Using the resource...")# 即使这里出现异常,__exit__也会被调用
# 离开with代码块后,__exit__自动调用
2. 显式清理方法
提供一個显式的 .close()
或 .cleanup()
方法,并要求调用者在使用完毕后调用它。通常与 try...finally
结合使用。
class FileHandler:def __init__(self, filename):self.file = open(filename, 'w')def write_data(self, data):self.file.write(data)def close(self): # 显式清理方法if self.file:self.file.close()self.file = Noneprint("File closed explicitly.")# 使用方式(保证会清理)
handler = FileHandler("test.txt")
try:handler.write_data("Hello")
finally:handler.close() # 确保无论发生什么都会执行关闭
总结
方法 | 可靠性 | 说明 |
---|---|---|
__del__ 方法 | 低 | 不保证执行,尤其怕强制终止。应避免用于关键清理。 |
with 语句 | 高 | 最强推荐。能处理异常,保证清理代码执行。 |
try...finally | 高 | 与显式清理方法配合,非常可靠。 |
atexit | 中 | 只在Python正常退出时工作,怕 SIGKILL 。 |
结论:不要依赖 __del__
来做任何重要的清理工作。 对于文件、网络连接、数据库连接等资源的释放,永远优先使用上下文管理器 (with
语句) 或 try...finally
块。