为什么 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 进程释放的内存的一部分,因此,我们出现分段错误。