📢 转载信息
原文链接:https://machinelearningmastery.com/everything-you-need-to-know-about-how-python-manages-memory/
原文作者:Bala Priya C
在本文中,您将了解到 Python 如何使用引用计数和分代垃圾回收来分配、跟踪和回收内存,以及如何使用 gc 模块来检查这种行为。
我们将涵盖的主题包括:
- 引用的作用以及在常见场景中 Python 引用计数如何变化。
- 纯引用计数下循环引用为何会导致泄漏,以及循环引用如何被回收。
gc模块的实际用途,用于观察阈值、计数和回收情况。
让我们直接开始吧。
Everything You Need to Know About How Python Manages Memory
Image by Editor
引言
在 C 等语言中,您需要手动分配和释放内存。忘记释放内存就会导致泄漏。释放两次则会导致程序崩溃。Python 通过自动垃圾回收为您处理了这种复杂性。您创建对象,使用它们,当它们不再需要时,Python 会清理它们。
但“自动”并不意味着“魔术”。了解 Python 的垃圾回收器如何工作,可以帮助您编写更高效的代码、调试内存泄漏并优化性能关键型应用程序。在本文中,我们将探讨引用计数、分代垃圾回收,以及如何使用 Python 的 gc 模块。以下是您将学到的内容:
- 什么是引用,以及引用计数在 Python 中如何工作
- 什么是循环引用以及它们为何会成为问题
- Python 的分代垃圾回收
- 使用
gc模块检查和控制回收
让我们开始吧。
Python 中的引用是什么?
在转向垃圾回收之前,我们需要了解“引用”到底是什么。
当您编写如下代码时:
|
1
|
x = 123
|
实际发生的情况如下:
- Python 在内存中的某个位置创建一个整数对象 123
- 变量
x存储指向该对象内存位置的指针 x并不“包含”整数值——它指向它
所以在 Python 中,变量是标签,而不是盒子。变量不保存值;它们是指向内存中对象的名称。将对象想象成漂浮在内存中的气球,将变量想象成系在这些气球上的绳子。多条绳子可以系在同一个气球上。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# Create an object
my_list = [1, 2, 3] # my_list points to a list object in memory
# Create another reference to the SAME object
another_name = my_list # another_name points to the same list
# They both point to the same object
print(my_list is another_name)
print(id(my_list) == id(another_name))
# Modifying through one affects the other (same object!)
my_list.append(4)
print(another_name)
# But reassigning creates a NEW reference
my_list = [5, 6, 7] # my_list now points to a DIFFERENT object
print(another_name)
|
当您编写 another_name = my_list 时,您并不是在复制列表。您是在为同一对象创建另一个指针。两个变量都引用(指向)内存中的同一个列表。这就是为什么通过一个变量进行的更改会出现在另一个变量中的原因。因此,上述代码将产生以下输出:
|
1
2
3
4
|
True
True
[1, 2, 3, 4]
[1, 2, 3, 4]
|
id() 函数显示对象的内存地址。当两个变量具有相同的 id() 时,它们引用的是同一个对象。
好的,那什么是“循环”引用?
当对象相互引用,形成一个循环时,就发生了循环引用。这是一个非常简单的例子:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
class Person:
def __init__(self, name):
self.name = name
self.friend = None # Will store a reference to another Person
# Create two people
alice = Person("Alice")
bob = Person("Bob")
# Make them friends - this creates a circular reference
alice.friend = bob # Alice's object points to Bob's object
bob.friend = alice # Bob's object points to Alice's object
|
现在我们有了一个循环:alice → Person(“Alice”) → .friend → Person(“Bob”) → .friend → Person(“Alice”) → …
这就是为什么它被称为“循环”(以防您还没猜到)。如果您顺着引用链追踪,您会进入一个循环:Alice 的对象引用 Bob 的对象,Bob 的对象引用 Alice 的对象,以此类推…… 这是一个无限循环。
Python 如何使用引用计数和分代垃圾回收来管理内存
Python 使用两种主要机制进行垃圾回收:
- 引用计数 (Reference counting):这是主要方法。当对象的引用计数达到零时,对象被删除。
- 分代垃圾回收 (Generational garbage collection):一个备份系统,用于查找和清理引用计数无法处理的循环引用。
让我们详细探讨两者。
引用计数的工作原理
每个 Python 对象都有一个引用计数,即指向它的引用数量,指的是指向它的变量(或其他对象)。当引用计数达到零时,内存立即被释放。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import sys
# Create an object - reference count is 1
my_list = [1, 2, 3]
print(f"Reference count: {sys.getrefcount(my_list)}")
# Create another reference - count increases
another_ref = my_list
print(f"Reference count: {sys.getrefcount(my_list)}")
# Delete one reference - count decreases
del another_ref
print(f"Reference count: {sys.getrefcount(my_list)}")
# Delete the last reference - object is destroyed
del my_list
|
输出:
|
1
2
3
|
Reference count: 2
Reference count: 3
Reference count: 2
|
引用计数的工作方式如下。Python 会对每个对象维护一个计数器,跟踪有多少引用指向它。每次您:
- 将对象赋给变量 → 计数增加
- 将其传递给函数 → 计数暂时增加
- 将其存储在容器中 → 计数增加
- 删除一个引用 → 计数减少
当计数达到零(没有剩余引用)时,Python 会立即释放内存。
📑 关于
sys.getrefcount():sys.getrefcount()显示的计数总是比您预期的要高 1,因为将对象传递给函数会创建一个临时引用。如果您看到“2”,实际上只有 1 个外部引用。
示例:引用计数实战
让我们用一个自定义类来看看引用计数的工作情况,该类会在被删除时发出通知。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
class DataObject:
"""Object that announces when it's created and destroyed"""
def __init__(self, name):
self.name = name
print(f"Created {self.name}")
def __del__(self):
"""Called when object is about to be destroyed"""
print(f"Deleting {self.name}")
# Create and immediately lose reference
print("Creating object 1:")
obj1 = DataObject("Object 1")
print("\nCreating object 2 and deleting it:")
obj2 = DataObject("Object 2")
del obj2
print("\nReassigning obj1:")
obj1 = DataObject("Object 3")
print("\nFunction scope test:")
def create_temporary():
temp = DataObject("Temporary")
print("Inside function")
create_temporary()
print("After function")
print("\nScript ending...")
|
在这里,当对象的引用计数达到零时,__del__ 方法(析构函数)就会被调用。在引用计数中,这会立即发生。
输出:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Creating object 1:
Created Object 1
Creating object 2 and deleting it:
Created Object 2
Deleting Object 2
Reassigning obj1:
Created Object 3
Deleting Object 1
Function scope test:
Created Temporary
Inside function
Deleting Temporary
After function
Script ending...
Deleting Object 3
|
请注意,Temporary 在函数退出时立即被删除,因为局部变量 temp 超出作用域。当 temp 消失时,对象不再有引用,因此它立即被释放。
Python 如何处理循环引用
如果您仔细跟踪了前面的内容,您会发现引用计数无法处理循环引用。让我们看看原因。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import gc
import sys
class Node:
def __init__(self, name):
self.name = name
self.reference = None
def __del__(self):
print(f"Deleting {self.name}")
# Create two separate objects
print("Creating two nodes:")
node1 = Node("Node 1")
node2 = Node("Node 2")
# Now create the circular reference
print("\nCreating circular reference:")
node1.reference = node2
node2.reference = node1
print(f"Node 1 refcount: {sys.getrefcount(node1) - 1}")
print(f"Node 2 refcount: {sys.getrefcount(node2) - 1}")
# Delete our variables
print("\nDeleting our variables:")
del node1
del node2
print("Objects still alive! (reference counts aren't zero)")
print("They only reference each other, but counts are still 1 each")
|
当您尝试删除这些对象时,仅靠引用计数无法清理它们,因为它们使彼此保持“存活”状态。即使没有外部变量引用它们,它们仍然相互引用。因此,它们的引用计数永远不会达到零。
输出:
|
1
2
3
4
5
6
7
8
9
|
Creating two nodes:
Creating circular reference:
Node 1 refcount: 2
Node 2 refcount: 2
Deleting our variables:
Objects still alive! (reference counts aren't zero)
They only reference each other, but counts are still 1 each
|
这里是对引用计数在此处无效的详细分析:
- 当我们删除
node1和node2变量后,对象仍然存在于内存中 - Node 1 的对象有一个引用(来自 Node 2 的
.reference属性) - Node 2 的对象有一个引用(来自 Node 1 的
.reference属性) - 每个对象的引用计数都是 1(不是 0),所以它们没有被释放
- 但是现在没有任何代码可以到达这些对象了!它们是垃圾,但引用计数无法检测到它们。
这就是为什么 Python 需要第二种垃圾回收机制来查找和清理这些循环。以下是如何手动触发垃圾回收以查找循环并像这样删除对象的方法:
🚀 想要体验更好更全面的AI调用?
欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。
评论区