为什么 python 的 "gc.collect()" 没有按预期工作?
Why does python's "gc.collect()" not work as expected?
这是我的测试代码:
#! /usr/bin/python3
import gc
import ctypes
name = "a" * 50
name_id = id(name)
del name
gc.collect()
print(ctypes.cast(name_id, ctypes.py_object).value)
输出:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
在我看来,gc.collect()
应该清除变量 name
及其值,
但是为什么我可以在 gc.collect()
之后用 name_id
获得价值?
你不应该期望gc.collect()
在这里做任何事情。 gc
只是控制循环垃圾收集器,这是一个辅助垃圾收集器,因为 CPython 使用引用计数作为其主要内存管理策略。循环垃圾收集器处理引用循环,这里没有引用循环所以gc.collect
不会做任何事情。
In my opinion, gc.collect() should clean the variable name and it's
value,
这根本不是 Python 的工作方式。 变量 不再随着 del name
退出,但 对象 继续存在,在这种情况下,由于编译器优化。 Python 变量不像 C 变量,它们不是内存块,它们是引用特定命名空间中对象的名称。
无论如何,反汇编代码会让您在这里有所了解:
In [1]: import dis
In [2]: dis.dis("""
...: import gc
...: import ctypes
...:
...: name = "a" * 50
...: name_id = id(name)
...: del name
...: gc.collect()
...: print(ctypes.cast(name_id, ctypes.py_object).value)
...: """)
2 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (gc)
6 STORE_NAME 0 (gc)
3 8 LOAD_CONST 0 (0)
10 LOAD_CONST 1 (None)
12 IMPORT_NAME 1 (ctypes)
14 STORE_NAME 1 (ctypes)
5 16 LOAD_CONST 2 ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
18 STORE_NAME 2 (name)
6 20 LOAD_NAME 3 (id)
22 LOAD_NAME 2 (name)
24 CALL_FUNCTION 1
26 STORE_NAME 4 (name_id)
7 28 DELETE_NAME 2 (name)
8 30 LOAD_NAME 0 (gc)
32 LOAD_METHOD 5 (collect)
34 CALL_METHOD 0
36 POP_TOP
9 38 LOAD_NAME 6 (print)
40 LOAD_NAME 1 (ctypes)
42 LOAD_METHOD 7 (cast)
44 LOAD_NAME 4 (name_id)
46 LOAD_NAME 1 (ctypes)
48 LOAD_ATTR 8 (py_object)
50 CALL_METHOD 2
52 LOAD_ATTR 9 (value)
54 CALL_FUNCTION 1
56 POP_TOP
58 LOAD_CONST 1 (None)
60 RETURN_VALUE
因此,当您的代码块被 编译 时,CPython 编译器注意到 "a"*50
可以变成一个常量,所以它做到了.它存储代码对象的常量,直到该代码对象不再存在(在这种情况下,当解释器存在时)。由于此代码对象将维护对此字符串对象的引用,因此它将一直存在。
所以,更明确地说:
In [4]: code = compile("""name = "a" * 50""", filename='foo', mode='exec')
In [5]: code
Out[5]: <code object <module> at 0x7ff7c12495d0, file "foo", line 1>
In [6]: code.co_consts
Out[6]: ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', None)
另请注意,Python 内存管理非常复杂且非常不透明。所有对象都在私有管理的堆上处理。仅仅因为一个对象被“释放”并不意味着运行时不会简单地 re-used 根据需要为相同类型(或其他合适类型)的对象分配那一点内存。看看这个:
In [1]: class Foo: pass
In [2]: import ctypes
In [3]: foo = Foo()
In [4]: id(foo)
Out[4]: 140559250737552
In [5]: del foo
In [6]: foo2 = Foo()
In [7]: id(foo2)
Out[7]: 140559250737680
In [8]: ctypes.cast(140559250737552, ctypes.py_object).value
Out[8]: <prompt_toolkit.lexers.pygments.RegexSync at 0x7fd68035c990>
In [9]: id(foo2)
Out[9]: 140559250737680
In [10]: del foo2
In [11]: ctypes.cast(140559250737680, ctypes.py_object).value
Out[11]: <prompt_toolkit.lexers.pygments.PygmentsLexer at 0x7fd68035ca10>
请注意在这些情况下您如何能够恢复一些对象,因为ipython交互式shell一直在创建对象,并且内部堆很高兴 re-use 那段内存。
看看更多 bare-bones REPL 中发生了什么:
(base) juanarrivillaga@50-254-139-253-static% python
Python 3.7.9 (default, Aug 31 2020, 07:22:35)
[Clang 10.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> class Foo: pass
...
>>> foo = Foo()
>>> i = id(foo)
>>> del foo
>>> ctypes.cast(i, ctypes.py_object).value
zsh: segmentation fault python
是的。更令人 期望 ,我们试图访问不仅被内部堆回收,而且被 Python 进程释放的内存的一部分,因此,我们出现分段错误。
这是我的测试代码:
#! /usr/bin/python3
import gc
import ctypes
name = "a" * 50
name_id = id(name)
del name
gc.collect()
print(ctypes.cast(name_id, ctypes.py_object).value)
输出:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
在我看来,gc.collect()
应该清除变量 name
及其值,
但是为什么我可以在 gc.collect()
之后用 name_id
获得价值?
你不应该期望gc.collect()
在这里做任何事情。 gc
只是控制循环垃圾收集器,这是一个辅助垃圾收集器,因为 CPython 使用引用计数作为其主要内存管理策略。循环垃圾收集器处理引用循环,这里没有引用循环所以gc.collect
不会做任何事情。
In my opinion, gc.collect() should clean the variable name and it's value,
这根本不是 Python 的工作方式。 变量 不再随着 del name
退出,但 对象 继续存在,在这种情况下,由于编译器优化。 Python 变量不像 C 变量,它们不是内存块,它们是引用特定命名空间中对象的名称。
无论如何,反汇编代码会让您在这里有所了解:
In [1]: import dis
In [2]: dis.dis("""
...: import gc
...: import ctypes
...:
...: name = "a" * 50
...: name_id = id(name)
...: del name
...: gc.collect()
...: print(ctypes.cast(name_id, ctypes.py_object).value)
...: """)
2 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (gc)
6 STORE_NAME 0 (gc)
3 8 LOAD_CONST 0 (0)
10 LOAD_CONST 1 (None)
12 IMPORT_NAME 1 (ctypes)
14 STORE_NAME 1 (ctypes)
5 16 LOAD_CONST 2 ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
18 STORE_NAME 2 (name)
6 20 LOAD_NAME 3 (id)
22 LOAD_NAME 2 (name)
24 CALL_FUNCTION 1
26 STORE_NAME 4 (name_id)
7 28 DELETE_NAME 2 (name)
8 30 LOAD_NAME 0 (gc)
32 LOAD_METHOD 5 (collect)
34 CALL_METHOD 0
36 POP_TOP
9 38 LOAD_NAME 6 (print)
40 LOAD_NAME 1 (ctypes)
42 LOAD_METHOD 7 (cast)
44 LOAD_NAME 4 (name_id)
46 LOAD_NAME 1 (ctypes)
48 LOAD_ATTR 8 (py_object)
50 CALL_METHOD 2
52 LOAD_ATTR 9 (value)
54 CALL_FUNCTION 1
56 POP_TOP
58 LOAD_CONST 1 (None)
60 RETURN_VALUE
因此,当您的代码块被 编译 时,CPython 编译器注意到 "a"*50
可以变成一个常量,所以它做到了.它存储代码对象的常量,直到该代码对象不再存在(在这种情况下,当解释器存在时)。由于此代码对象将维护对此字符串对象的引用,因此它将一直存在。
所以,更明确地说:
In [4]: code = compile("""name = "a" * 50""", filename='foo', mode='exec')
In [5]: code
Out[5]: <code object <module> at 0x7ff7c12495d0, file "foo", line 1>
In [6]: code.co_consts
Out[6]: ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', None)
另请注意,Python 内存管理非常复杂且非常不透明。所有对象都在私有管理的堆上处理。仅仅因为一个对象被“释放”并不意味着运行时不会简单地 re-used 根据需要为相同类型(或其他合适类型)的对象分配那一点内存。看看这个:
In [1]: class Foo: pass
In [2]: import ctypes
In [3]: foo = Foo()
In [4]: id(foo)
Out[4]: 140559250737552
In [5]: del foo
In [6]: foo2 = Foo()
In [7]: id(foo2)
Out[7]: 140559250737680
In [8]: ctypes.cast(140559250737552, ctypes.py_object).value
Out[8]: <prompt_toolkit.lexers.pygments.RegexSync at 0x7fd68035c990>
In [9]: id(foo2)
Out[9]: 140559250737680
In [10]: del foo2
In [11]: ctypes.cast(140559250737680, ctypes.py_object).value
Out[11]: <prompt_toolkit.lexers.pygments.PygmentsLexer at 0x7fd68035ca10>
请注意在这些情况下您如何能够恢复一些对象,因为ipython交互式shell一直在创建对象,并且内部堆很高兴 re-use 那段内存。
看看更多 bare-bones REPL 中发生了什么:
(base) juanarrivillaga@50-254-139-253-static% python
Python 3.7.9 (default, Aug 31 2020, 07:22:35)
[Clang 10.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> class Foo: pass
...
>>> foo = Foo()
>>> i = id(foo)
>>> del foo
>>> ctypes.cast(i, ctypes.py_object).value
zsh: segmentation fault python
是的。更令人 期望 ,我们试图访问不仅被内部堆回收,而且被 Python 进程释放的内存的一部分,因此,我们出现分段错误。