嵌入式Python调用Py_CompileString等API时对象无法释放问题
问题根因
这不是Python的预期行为,是嵌入式场景下模块卸载流程缺失关键步骤导致的引用残留,核心问题出在PyImport_ExecCodeModule()的隐式行为:
- 该API执行时会自动将生成的模块对象以传入的模块名为键,存入全局模块缓存
sys.modules,并持有1个强引用。你自己代码里拿到的模块指针只是第二个引用,仅释放自己持有的引用时,sys.modules里的引用始终存在,模块引用计数永远剩1,不会被销毁。
你观察到的引用计数现象完全可以顺着这个逻辑解释:
- 编译得到的code对象初始引用计数为1:该对象仅由你的代码持有,没有被Python内部全局结构引用,调用
Py_DECREF()后引用计数归0直接释放,此时指针变为悬空野值是正常表现,不要在对象释放后再访问该指针。 - 模块、函数、返回值初始引用计数为2:
- 1个引用来自你调用API时拿到的返回值,由你的C++代码持有
- 剩余1个引用来自Python内部的引用链:模块被
sys.modules持有,模块内定义的python_main函数被模块的__dict__属性字典持有,返回值如果被模块全局作用域、闭包或内部缓存引用,也会随模块一同留存
- 你当前的“重编译生效”本质是在旧模块的命名空间内执行新编译的代码,新代码覆盖了同名的顶层变量(比如
python_main函数),但旧模块本身从未被销毁,未被覆盖的旧对象、被其他路径引用的旧对象会一直残留,既会造成内存泄漏,也可能出现旧脚本注册的回调、全局状态未被清理的异常。
正确的模块清理/重加载流程
要实现旧模块彻底销毁、从零初始化的效果,重编译前必须按以下步骤操作:
- 从全局模块缓存中删除对应模块的条目,释放全局持有的强引用
// PyImport_GetModuleDict返回borrowed引用,无需手动释放 PyObject* sys_modules = PyImport_GetModuleDict(); // 替换为你实际使用的模块名 if (PyDict_DelItemString(sys_modules, "user_script") < 0) { // 模块不存在时忽略异常即可 PyErr_Clear(); } - 释放你代码中持有的所有相关对象的强引用,包括但不限于:之前拿到的脚本返回值对象、
python_main函数对象、模块对象、编译得到的code对象。所有Py_CompileString/PyImport_ExecCodeModule/PyObject_GetAttrString/PyObject_Call*返回的新引用,用完都要对应调用Py_DECREF()。 - 调用
PyGC_Collect()触发一次全量垃圾回收,处理脚本对象间可能存在的循环引用——这类引用无法靠引用计数机制自动释放,必须由GC遍历回收。
额外注意事项
- 如果需要频繁重加载脚本,不要反复使用同一个模块名:哪怕清理流程完全正确,旧模块里的对象如果被其他全局结构(比如注册的C回调、其他导入了该模块的扩展模块、Python内部类型缓存)持有引用,依然无法完全回收。可以每次重加载时给模块加自增后缀生成唯一名称(比如
user_script_1/user_script_2),同时定期清理sys.modules里的过期条目,避免无限制的内存增长。 - 不要在对象调用
Py_DECREF()后再访问其内容:当引用计数归0时Python会立即释放对象内存,此时原指针是悬空野值,读取任何字段都是未定义行为。 - 检查所有Python API返回值的引用类型:部分API返回borrowed引用(比如
PyImport_GetModuleDict、PyList_GetItem)不需要手动DECREF,部分返回新引用必须手动DECREF,错漏都会导致引用计数异常。
内容的提问来源于stack exchange,提问作者Mark Coniglio
相关产品推荐
相关产品推荐

