为什么python的“gc.collect()”不能像预期的那样工作?

b09cbbtk  于 2023-01-06  发布在  Python
关注(0)|答案(1)|浏览(216)

下面是我的测试代码:

#! /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可以得到值呢?

t3irkdon

t3irkdon1#

你不应该 * 期望 * gc.collect()在这里做 * 任何事情 *。gc只是控制循环垃圾收集器,这是一个辅助垃圾收集器,因为CPython使用引用计数作为它的主内存管理策略。循环垃圾收集器处理引用周期,这里 * 没有引用周期 ,所以gc.collect不会做任何事情。
在我看来,gc.collect()应该清除变量名和它的值,
Python根本不是这样工作的。variable 不再存在于del name中,但是 object 继续存在,在这种情况下,是由于编译器优化。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的内存管理是复杂的并且相当不透明的。所有的对象都在一个私有管理的堆上处理。仅仅因为一个对象被“释放”并不意味着运行时不会简单地根据需要为相同类型(或者其他合适的类型)的对象重用这一点内存。看看这个:

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一直在创建对象,并且内部堆乐于重用这些内存。
看看在一个更简单的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进程释放,因此,我们得到了一个分段错误。

相关问题