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 实例

通过引用计数和垃圾回收机制,内存不会被耗尽,程序可以永远的运行下去。