1
0
wiki/FormalSciences/ComputerScience/ProgrammingLanguage/Python/2.核心进阶/2.6-垃圾回收机制.md
2024-10-13 20:52:05 +08:00

223 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 垃圾回收
description: python 垃圾回收机制
keywords:
- Python
- 垃圾回收
tags:
- Python/进阶
- 技术/程序语言
author: 7Wate
date: 2023-10-27
---
## 引用和引用计数
### Python 中的引用概念
**在 Python 中,所有的数据都是以对象的形式存在。**当我们创建一个变量并赋值时,实际上 Python 会为我们创建一个对象,然后变量会引用这个对象。这种关系,我们称之为引用。
例如:
```python
a = 1
```
这里,数字 `1` 是一个 `int` 类型的对象,变量 `a` 是对这个对象的引用。
### 引用计数的基本原理
**Python 使用一种叫做引用计数的方式来管理内存。**每一个对象都有一个引用计数,用来记录有多少个引用指向这个对象。当这个计数值变为 0 时, Python 就知道没有任何引用指向这个对象,这个对象就可以被安全地销毁了,它占用的内存就会被释放。
例如:
```python
a = 1 # 引用计数为1
b = a # 引用计数为2
a = None # 引用计数为1
b = None # 引用计数为0对象被销毁
```
### 引用计数的增减和对象的创建与销毁过程
```mermaid
graph LR
A[Python 对象] --> |创建| B(引用计数增加)
B --> C{引用计数是否为0?}
C -->|是| D[对象被立即回收]
C -->|否| E[对象继续存在]
E --> F{是否存在循环引用?}
F --> |是| G[标记-清除处理]
G --> H[分代回收处理]
F --> |否| I[对象持续存在]
H --> J{是否到达阈值?}
J -->|是| K[触发垃圾回收]
J -->|否| I
K --> L[回收结束,对象被销毁]
```
每当我们创建一个新的引用(赋值操作),对象的引用计数就会增加 1。当我们删除一个引用例如赋值为 `None` 或者使用 `del` 命令),对象的引用计数就会减少 1。当引用计数变为 0 时Python 的垃圾回收器就会销毁这个对象并回收它所占用的内存。
## 循环引用和引用计数的限制
### 循环引用的概念和问题
**循环引用是指两个或更多的对象互相引用,形成一种闭环。**在这种情况下,即使没有其他引用指向这些对象,它们的引用计数也永远不会变为 0所以它们不会被 Python 的垃圾回收器销毁,导致内存泄漏。
例如:
```python
class Node:
def __init__(self):
self.other = None
a = Node()
b = Node()
a.other = b # a 引用了 b
b.other = a # b 引用了 a形成了循环引用
```
### 引用计数无法解决的循环引用问题
这正是引用计数法的一个主要弱点。尽管它简单易懂,但它不能处理循环引用的问题。在上面的例子中,即使我们删除了对 `a``b` 的引用,它们也不会被销毁,因为它们互相引用,它们的引用计数永远不会变为 0。
```python
del a
del b
# 现在a 和 b 形成的循环引用对象仍然存在,但我们无法访问它们
```
### 引用计数的弱点和限制
引用计数法的另一个弱点是它的开销相对较大。每次创建或删除引用时Python 都需要更新引用计数。这可能在大量对象创建和销毁的情况下成为性能瓶颈。
## 垃圾回收算法
### 垃圾回收算法的概述
为了解决引用计数法不能处理循环引用的问题Python 引入了两种垃圾回收算法:**标记 - 清除算法和分代回收算法。这两种算法都是为了检测和回收循环引用的对象。**
### 标记 - 清除算法的原理和流程
标记 - 清除算法是一种基础的垃圾回收算法。它的**基本原理是通过标记和清除两个步骤来回收垃圾对象。**在标记步骤中,从某些根对象(例如全局变量)出发,遍历所有可达的对象,将这些对象标记为活动。剩下的未被标记的对象(即不可达的对象)就被认为是垃圾。然后在“清除”步骤中,清除所有标记为“垃圾”的对象。
在 Python 中,标记 - 清除算法主要用于检测和清除循环引用对象。它的工作流程是这样的:
1. 从所有的容器对象(例如列表、字典和类实例等)出发,找出所有可能形成循环引用的对象。
2. 对这些对象应用标记 - 清除算法,找出并清除真正的循环引用对象。
### 分代回收算法的原理和优化
分代回收是 Python 用来优化垃圾回收性能的一种方式。它的**基本思想是将所有的对象分为几代,每一代的对象有自己的生命周期和回收策略。**新创建的对象被放入第一代,当这一代的对象经历了一定次数的垃圾回收后仍然存活,就被移到下一代。每一代的垃圾回收频率都不同,通常,越年轻的代的垃圾回收频率越高。
Python 的分代回收有三代。新创建的对象被放入第一代(`generation 0`),当它经历了一次垃圾回收后仍然存活,就被移到第二代(`generation 1`)。同样,第二代的对象在经历了一次垃圾回收后仍然存活,就被移到第三代(`generation 2`)。第三代的对象在经历了一次垃圾回收后仍然存活,就留在第三代。每一代的垃圾回收频率都不同,第一代的频率最高,第三代的频率最低。
分代回收算法的优点是它可以减少垃圾回收的开销,因为经常产生垃圾的通常是生命周期短的对象(例如临时变量),而生命周期长的对象(例如全局变量)很少产生垃圾。这种方式可以让 Python 更加聚焦于可能产生垃圾的地方,从而提高垃圾回收的效率。
### Python 中的垃圾回收算法实现
```mermaid
sequenceDiagram
participant O as Python对象
participant RC as 引用计数
participant GM as 垃圾回收机制
participant GC as gc模块
O->>RC: 创建对象,引用计数+1
RC-->>O: 维持对象
RC->>GM: 检查引用计数是否为0
GM-->>RC: 不为0继续维持
RC->>GM: 引用计数变为0
GM-->>O: 对象被立即回收
GM->>GC: 检查是否存在循环引用
GC-->>GM: 存在循环引用
GM->>GC: 触发标记-清除
GC-->>GM: 清除循环引用
GM->>GC: 触发分代回收
GC-->>GM: 分代回收完成
GM->>GC: 检查是否达到阈值
GC-->>GM: 达到阈值
GM->>O: 触发垃圾回收,对象被销毁
GM->>GC: 检查是否达到阈值
GC-->>GM: 没有达到阈值
GM-->>GC: 继续监视
```
Python 的垃圾回收机制是基于引用计数的,当对象的引用计数降到 0 时该对象就会被立即回收。但是对于循环引用的问题Python 使用标记 - 清除和分代回收两种算法来解决。
首先Python 使用标记 - 清除算法来检测和清除循环引用的对象。然后为了优化垃圾回收的性能Python 会根据对象的存活时间将它们分成三代并分别进行回收。这样Python 就能有效地管理内存,同时尽可能地降低垃圾回收的开销。
## Gc 模块
Python 通过 `gc` 模块提供了对垃圾回收机制的直接控制。`gc` 模块提供了一些函数,让我们可以手动触发垃圾回收,查询垃圾回收的状态,或者调整垃圾回收的参数。
### 基础功能
下面是一些常用的 `gc` 函数:
| 函数 | 描述 |
| :----------------------------------------------------------- | :----------------------------------------------------------- |
| `gc.enable()` | 启用自动垃圾回收。 |
| `gc.disable()` | 禁用自动垃圾回收。 |
| `gc.isenabled()` | 查看自动垃圾回收是否被启用。 |
| `gc.collect(generation=2)` | 立即进行一次垃圾回收。可以通过 `generation` 参数指定要收集的代的编号0 代表最年轻的一代2 代表所有代)。 |
| `gc.set_threshold(threshold0, threshold1=None, threshold2=None)` | 设置垃圾回收的阈值。当某一代的垃圾数量超过对应的阈值时,就会触发垃圾回收。 |
| `gc.get_threshold()` | 获取当前的垃圾回收阈值。 |
| `gc.get_count()` | 获取当前每一代的垃圾数量。 |
| `gc.get_objects()` | 返回一个列表,包含所有当前被监视的对象。 |
| `gc.get_stats()` | 返回一个字典,包含垃圾回收的统计信息。 |
| `gc.set_debug(flags)` | 设置垃圾回收的调试标志。 |
| `gc.get_debug()` | 获取当前的垃圾回收调试标志。 |
| `gc.get_referents(*objs)` | 返回一个列表,包含所有给定对象的直接引用对象。 |
| `gc.get_referrers(*objs)` | 返回一个列表,包含所有直接引用给定对象的对象。 |
下面是一个简单的例子,演示了如何使用 `gc` 模块:
```python
import gc
# 手动触发垃圾回收
gc.collect()
# 获取当前的计数器值
print(gc.get_count())
# 获取当前的阈值
print(gc.get_threshold())
# 设置新的阈值
gc.set_threshold(500)
# 获取所有存在的对象
print(len(gc.get_objects()))
# 获取统计信息
print(gc.get_stats())
```
### 高级用法
除了上述的基本功能,`gc` 模块还提供了一些高级功能,例如你可以注册自己的回调函数,当垃圾回收发生时,这些回调函数就会被调用。这可以用来监控垃圾回收的过程,或者调试内存泄漏的问题。
下面是一个例子,演示了如何注册回调函数:
```python
import gc
def callback(phase, info):
print(f"{phase}: {info}")
# 注册回调函数
gc.callbacks.append(callback)
# 触发垃圾回收
gc.collect()
```
在这个例子中,每次垃圾回收发生时,`callback` 函数都会被调用,它会打印出垃圾回收的阶段和一些信息。
## 总结
Python 的垃圾回收机制是基于引用计数的它简单高效但无法处理循环引用的问题。为了解决这个问题Python 引入了标记 - 清除和分代回收两种垃圾回收算法。这两种算法可以有效地检测和清除循环引用的对象,同时优化垃圾回收的性能。
Python 通过 `gc` 模块提供了对垃圾回收机制的直接控制。通过 `gc` 模块,我们可以手动触发垃圾回收,查询垃圾回收的状态,或者调整垃圾回收的参数。我们甚至可以注册自己的回调函数,以便在垃圾回收发生时获取通知。