Python 的内存管理与垃圾回收
1. 内存管理概述
1.1 手动内存管理
在计算机发展的早期,编程语言提供了手动内存管理的机制,例如 C 语言,提供了用于分配和释放的函数 malloc 和 free,如下所示:
#include <stdlib.h>
void *malloc(size_t size);
void free(void *p);
- 函数 malloc 分配指定大小 size 的内存,返回内存的首地址
- 函数 free 释放之前申请的内存
程序员负责保证内存管理的正确性:使用 malloc 申请一块内存后,如果不再使用,需要使用 free 将其释放,示例如下:
#include <stdlib.h>
void test()
{
void *p = malloc(10);
访问 p 指向的内存区域;
free(p);
}
int main()
{
test();
}
- 使用 malloc(10) 分配一块大小为 10 个字节的内存区域
- 使用 free§ 释放这块内存区域
如果忘记释放之前使用 malloc 申请的内存,则会导致可用内存不断减少,这种现象被称为 “内存泄漏”,示例如下:
#include <stdio.h>
#include <stdlib.h>
void test()
{
void *p = malloc(10);
访问 p 指向的内存区域;
}
int main()
{
while (1)
test();
}
- 在函数 test 中,使用 malloc 申请一块内存
- 但是使用完毕后,忘记释放了这块内存
- 在函数 main 中,循环调用函数 test()
- 每次调用函数 test(),都会造成内存泄漏
- 最终,会耗尽所有的内存
1.2 自动内存管理
在计算机发展的早期,硬件性能很差,为了最大程度的压榨硬件性能,编程语言提供了手动管理内存的机制。手动管理内存的机制的优点在于能够有效规划和利用内存,其缺点在于太繁琐了,很容易出错。
随着计算机的发展,硬件性能不断提高,这时候出现的编程语言,例如:Java、C#、PHP、Python,则提供了自动管理内存的机制:程序员申请内存后,不需要再显式的释放内存,由编程语言的解释器负责释放内存,从根本上杜绝了 “内存泄漏” 这类错误。
在下面的 Python 程序中,在无限循环中不断的申请内存:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
while True:
person = Person('tom', 13)
- 类 Person 包含两个属性:name 和 age
- 在 while 循环中,使用类 Person 生成一个实例 person
- 需要申请一块内存用于保存实例 person 的属性
Python 解释器运行这个程序时,发现实例 person 不再被引用后,会自动的释放 person 占用的空间。因此这个程序可以永远的运行下去,而不会把内存耗尽。
2. 基于引用计数的内存管理
2.1 基本原理
引用计数是一种最简单的自动内存管理机制:
- 每个对象都有一个引用计数
- 当把该对象赋值给一个变量时,对象的引用计数递增 1
引用计数的实例如下:
A = object()
B = A
A = None
B = None
- 在第 1 行,使用 object() 创建一个对象,变量 A 指向该对象
- 对象的引用计数变化为 1
- 在第 2 行,变量 B 指向相同的对象
- 对象的引用计数变化为 2
- 在第 3 行,变量 A 指向 None
- 对象的引用计数变化为 1
- 在第 3 行,变量 B 指向 None
- 对象的引用计数变化为 0
从图中可以看出,当变量 A 和变量 B 都不再指向对象时,对象的引用计数变为 0,系统检测到该对象成为废弃对象,可以将此废弃对象回收。
2.2 优点和缺点
引用计数的优点在于:
- 实现简单
- 系统检测到对象的引用计数变为 0 后,可以及时的释放废弃的对象
- 处理回收内存的时间分摊到了平时
引用计数的缺点在于:
- 维护引用计数消耗性能,每次变量赋值时,都需要维护维护引用计数
- 无法释放存在循环引用的对象
下面是一个存在循环引用的例子:
class Node:
def __init__(self, data, next):
self.data = data
self.next = next
node = Node(123, None)
node.next = node
node = None
- 在第 6 行,创建对象 node
- 对象 node 的 next 指向 None
- 此时对象 node 的引用计数为 1
- 在第 7 行,对象 node 的 next 指向 node 自身
- 此时对象 node 的引用计数为 2
- 在第 7 行,对象 node 指向 None
- 此时对象 node 的引用计数为 1
对象 node 的 next 字段指向自身,导致:即使没有外部的变量指向对象 node,对象 node 的引用计数也不会变为 0,因此对象 node 就永远不会被释放了。
3. 基于垃圾回收的内存管理
3.1 基本原理
垃圾回收是目前主流的内存管理机制:
- 通过一系列的称为 “GC Root” 的对象作为起始对象
- 从 GC Root 出发,进行遍历
- 最终将对象划分为两类:
- 从 GC Root 可以到达的对象
- 从 GC Root 无法到达的对象
从 GC Root 无法到达的对象被认为是废弃对象,可以被系统回收。
- 在 Python 语言中,可作为 GC Roots 的对象主要是指全局变量指向的对象。
- 从 GC Roots 出发,可以到达 object 1、object 2、object 3、object 4
- 从 GC Roots 出发,无法到达 object 5、object 6、object 7,它们被判定为可回收的对象
3.2 优点和缺点
垃圾回收的优点在于:
- 可以处理存在循环引用的对象
垃圾回收的缺点在于:
- 实现复杂
- 进行垃圾回收时,需要扫描程序中所有的对象,因此需要暂停程序的运行。当程序中对象数量较多时,暂停程序的运行时间过长,系统会有明显的卡顿现象。
4. Python 的内存管理机制
Python 的内存管理采用了混合的方法:
- Python 使用引用计数来保持追踪内存中的对象,当对象的引用计数为 0 时,回收该对象
- Python 同时使用垃圾回收机制来回收存在有循环引用的对象
下面的例子中,演示了 Python 的内存管理策略:
class Circular:
def __init__(self):
self.data = 0
self.next = self
class NonCircular:
def __init__(self):
self.data = 0
self.next = None
def hybrid():
while True:
circular = Circular()
nonCircular = NonCircular()
hybrid()
- 类 Circular,创建了一个包含循环引用的对象
- self.next 指向自身,导致了循环引用
- 类 Circular 的实例只能被垃圾回收机制释放
- 类 NonCircular,创建了一个不包含循环引用的对象
- self.next 指向 None,没有循环引用
- 类 NonCircular 的实例可以引用计数机制释放
- 在方法 hybrid 中
- 在无限循环中,不断的申请 Circular 实例和 NonCircular 实例
通过引用计数和垃圾回收机制,内存不会被耗尽,程序可以永远的运行下去。