我如何理解是否使用了内存地址?

How can I understand if a memory address is used or not?

我正在用 Python 垃圾收集器做一些实验,我想检查是否使用了内存地址。在下面的示例中,我取消引用了 ls[2] 处的字符串 (surely)。如果我运行垃圾收集器,我仍然可以在原始地址看到surely。我想确定地址现在是可写的。有没有办法在 Python 中检查它?

from ctypes import string_at
from sys import getsizeof
import gc
ls = ['This','will be','surely','deleted']
idsurely= id(ls[2]) 
sizesurely = getsizeof(ls[2])
ls[2] = 'probably'
print(ls)
print(string_at(idsurely,sizesurely))
gc.collect()
# I check there is nothing in the garbage
print(gc.garbage)
print(string_at(idsurely,sizesurely))

我主要是从理论的角度对此感兴趣,所以我并不是说它有实际用途。我的目标是展示内存如何在教程中工作。我想表明数据仍然存在,并且现在可以写入地址中的字节。所以脚本的输出到现在为止符合预期。我只是想证明最后一段。

来自关于 gc 的文档:

... the collector supplements the reference counting already used in Python...

来自 gc.is_tracked():

Returns True if the object is currently tracked by the garbage collector, False otherwise. As a general rule, instances of atomic types aren’t tracked and instances of non-atomic types (containers, user-defined objects…) are.

垃圾收集器跟踪字符串:

In [1]: import gc

In [2]: test = 'surely'
Out[2]: 'surely'

In [3]: gc.is_tracked(test)
Out[3]: False

查看文档,似乎没有一种方法可以从语言中访问引用计数

请注意,至少对我来说,使用 string_at 在交互式解释器中不起作用 。它在脚本中确实有效。

不可能。

Python 中没有已用或未用内存地址的中央注册表。甚至没有所有对象的中央注册表(循环 GC 不知道所有对象),即使您有所有对象的注册表,也不足以确定正在使用的内存位置.此外,您不能只读取任意内存地址,或写入任意已释放地址。这会很快导致段错误或更糟。

最后,我强烈建议不要在教程中使用这种东西,即使您确实找到了使它起作用的东西。当你把一些东西放在教程中时,阅读教程的大部分人会认为这是他们应该学习的东西。编程新手不应被误导,认为检查可能已解除分配的内存位置是他们应该做的事情。

您的实验偏离了基础。 id(仅作为 CPython 实现细节)确实获取了相关对象的内存地址,但我们谈论的是 Python 对象本身,而不是它包含的数据。 sys.getsizeof returns 一个大致对应于对象占用多少内存的数字,但是没有保证内存是连续的。

纯属巧合,这个 almost 适用于 str(尽管如果有问题的字符串缓存了其 UTF-8 或wchar_t 形式,所以你冒着让你的程序崩溃的风险),但即便如此你的测试还是有缺陷的; CPython 保留看起来像合法变量名称的字符串文字,因此如果有问题的字符串在程序的其他任何地方显示为文字(包括某些 class 的名称或您导入的某些模块中的函数),当您更换它时,它实际上不会消失。如果文字字符串出现在任何地方的任何函数中,就会出现类似的隐式缓存(它最终不仅被保留,而且存储在该函数的常量中)。

更新: 在测试中,在实际脚本中,当您持有它的副本时 'surely' 的引用计数是 3,这当您将其替换为 'probably' 时,它会下降到 2。事实证明,即使在全局范围内,常量也会被缓存。交互式解释器没有表现出这种行为的唯一原因是它有效地 eval 分开每一行,所以当 eval 完成时常量缓存被丢弃。

即使这一切都不是问题,大多数(几乎所有)内存管理器(CPython 的专用小对象堆和它所构建的通用堆)在以下情况下实际上不会将内存清零它已发布,因此如果您在它真正发布后不久查看相同的地址,它可能包含非常相似的数据。

最后,您的 gc.collect() 调用不会改变任何东西,除非巧合(在 gc 期间发生的任何事情都可能通过副作用分配内存)。 str 不是垃圾回收类型,因为它不能包含对其他 Python 对象的引用,所以它不可能是引用循环中的 link,而 CPython 垃圾收集器只关心收集循环垃圾; CPython 是引用计数的,因此当最后一个引用消失时,不属于引用循环的任何内容都会立即自动清除。

这一切导致的简短回答是:在 CPython 中,无法非启发式地确定特定内存地址是否已释放到空闲存储并可供重用。 CPython 的内存管理方案是纯粹的实现细节,在这种细节级别公开 API 会在人们依赖它们时产生兼容性问题。

最接近的方法是使用 the tracemalloc module 之类的方法来执行基本快照并计算快照中的差异。这不会让您 window 通过 AFAICT 了解特定地址是否仍在使用;充其量它可以告诉您肯定使用的地址是在哪里分配的。

您可以使用的另一种方法(特定于 CPython)是在替换对象之前检查引用计数; sys.getrefcount 对于给定的 name/attribute 报告 2,然后 deling(或重新绑定)name/attribute 将释放它(假设没有线程可能在两者之间创建额外的引用测试和 del/重新绑定)。您期望 2,而不是 1,因为调用 sys.getrefcount 会创建对相关对象的临时引用。如果它报告一个大于 2 的数字,deling/rebinding 仍然可能导致对象在循环垃圾收集器运行时最终被删除,如果该对象是引用循环的一部分,但是对于 2 的引用计数(或 1 对于其他未命名的内容,例如 sys.getrefcount(''.join(('f', '9')) 等),行为将是确定性的。