Python 的內(nèi)存管理與垃圾回收
1. 內(nèi)存管理概述
1.1 手動(dòng)內(nèi)存管理
在計(jì)算機(jī)發(fā)展的早期,編程語言提供了手動(dòng)內(nèi)存管理的機(jī)制,例如 C 語言,提供了用于分配和釋放的函數(shù) malloc 和 free,如下所示:
#include <stdlib.h>
void *malloc(size_t size);
void free(void *p);
- 函數(shù) malloc 分配指定大小 size 的內(nèi)存,返回內(nèi)存的首地址
- 函數(shù) free 釋放之前申請(qǐng)的內(nèi)存
程序員負(fù)責(zé)保證內(nèi)存管理的正確性:使用 malloc 申請(qǐng)一塊內(nèi)存后,如果不再使用,需要使用 free 將其釋放,示例如下:
#include <stdlib.h>
void test()
{
void *p = malloc(10);
訪問 p 指向的內(nèi)存區(qū)域;
free(p);
}
int main()
{
test();
}
- 使用 malloc(10) 分配一塊大小為 10 個(gè)字節(jié)的內(nèi)存區(qū)域
- 使用 free§ 釋放這塊內(nèi)存區(qū)域
如果忘記釋放之前使用 malloc 申請(qǐng)的內(nèi)存,則會(huì)導(dǎo)致可用內(nèi)存不斷減少,這種現(xiàn)象被稱為 “內(nèi)存泄漏”,示例如下:
#include <stdio.h>
#include <stdlib.h>
void test()
{
void *p = malloc(10);
訪問 p 指向的內(nèi)存區(qū)域;
}
int main()
{
while (1)
test();
}
- 在函數(shù) test 中,使用 malloc 申請(qǐng)一塊內(nèi)存
- 但是使用完畢后,忘記釋放了這塊內(nèi)存
- 在函數(shù) main 中,循環(huán)調(diào)用函數(shù) test()
- 每次調(diào)用函數(shù) test(),都會(huì)造成內(nèi)存泄漏
- 最終,會(huì)耗盡所有的內(nèi)存
1.2 自動(dòng)內(nèi)存管理
在計(jì)算機(jī)發(fā)展的早期,硬件性能很差,為了最大程度的壓榨硬件性能,編程語言提供了手動(dòng)管理內(nèi)存的機(jī)制。手動(dòng)管理內(nèi)存的機(jī)制的優(yōu)點(diǎn)在于能夠有效規(guī)劃和利用內(nèi)存,其缺點(diǎn)在于太繁瑣了,很容易出錯(cuò)。
隨著計(jì)算機(jī)的發(fā)展,硬件性能不斷提高,這時(shí)候出現(xiàn)的編程語言,例如:Java、C#、PHP、Python,則提供了自動(dòng)管理內(nèi)存的機(jī)制:程序員申請(qǐng)內(nèi)存后,不需要再顯式的釋放內(nèi)存,由編程語言的解釋器負(fù)責(zé)釋放內(nèi)存,從根本上杜絕了 “內(nèi)存泄漏” 這類錯(cuò)誤。
在下面的 Python 程序中,在無限循環(huán)中不斷的申請(qǐng)內(nèi)存:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
while True:
person = Person('tom', 13)
- 類 Person 包含兩個(gè)屬性:name 和 age
- 在 while 循環(huán)中,使用類 Person 生成一個(gè)實(shí)例 person
- 需要申請(qǐng)一塊內(nèi)存用于保存實(shí)例 person 的屬性
Python 解釋器運(yùn)行這個(gè)程序時(shí),發(fā)現(xiàn)實(shí)例 person 不再被引用后,會(huì)自動(dòng)的釋放 person 占用的空間。因此這個(gè)程序可以永遠(yuǎn)的運(yùn)行下去,而不會(huì)把內(nèi)存耗盡。
2. 基于引用計(jì)數(shù)的內(nèi)存管理
2.1 基本原理
引用計(jì)數(shù)是一種最簡(jiǎn)單的自動(dòng)內(nèi)存管理機(jī)制:
- 每個(gè)對(duì)象都有一個(gè)引用計(jì)數(shù)
- 當(dāng)把該對(duì)象賦值給一個(gè)變量時(shí),對(duì)象的引用計(jì)數(shù)遞增 1
引用計(jì)數(shù)的實(shí)例如下:
A = object()
B = A
A = None
B = None
- 在第 1 行,使用 object() 創(chuàng)建一個(gè)對(duì)象,變量 A 指向該對(duì)象
- 對(duì)象的引用計(jì)數(shù)變化為 1
- 在第 2 行,變量 B 指向相同的對(duì)象
- 對(duì)象的引用計(jì)數(shù)變化為 2
- 在第 3 行,變量 A 指向 None
- 對(duì)象的引用計(jì)數(shù)變化為 1
- 在第 3 行,變量 B 指向 None
- 對(duì)象的引用計(jì)數(shù)變化為 0
從圖中可以看出,當(dāng)變量 A 和變量 B 都不再指向?qū)ο髸r(shí),對(duì)象的引用計(jì)數(shù)變?yōu)?0,系統(tǒng)檢測(cè)到該對(duì)象成為廢棄對(duì)象,可以將此廢棄對(duì)象回收。
2.2 優(yōu)點(diǎn)和缺點(diǎn)
引用計(jì)數(shù)的優(yōu)點(diǎn)在于:
- 實(shí)現(xiàn)簡(jiǎn)單
- 系統(tǒng)檢測(cè)到對(duì)象的引用計(jì)數(shù)變?yōu)?0 后,可以及時(shí)的釋放廢棄的對(duì)象
- 處理回收內(nèi)存的時(shí)間分?jǐn)偟搅似綍r(shí)
引用計(jì)數(shù)的缺點(diǎn)在于:
- 維護(hù)引用計(jì)數(shù)消耗性能,每次變量賦值時(shí),都需要維護(hù)維護(hù)引用計(jì)數(shù)
- 無法釋放存在循環(huán)引用的對(duì)象
下面是一個(gè)存在循環(huán)引用的例子:
class Node:
def __init__(self, data, next):
self.data = data
self.next = next
node = Node(123, None)
node.next = node
node = None
- 在第 6 行,創(chuàng)建對(duì)象 node
- 對(duì)象 node 的 next 指向 None
- 此時(shí)對(duì)象 node 的引用計(jì)數(shù)為 1
- 在第 7 行,對(duì)象 node 的 next 指向 node 自身
- 此時(shí)對(duì)象 node 的引用計(jì)數(shù)為 2
- 在第 7 行,對(duì)象 node 指向 None
- 此時(shí)對(duì)象 node 的引用計(jì)數(shù)為 1
對(duì)象 node 的 next 字段指向自身,導(dǎo)致:即使沒有外部的變量指向?qū)ο?node,對(duì)象 node 的引用計(jì)數(shù)也不會(huì)變?yōu)?0,因此對(duì)象 node 就永遠(yuǎn)不會(huì)被釋放了。
3. 基于垃圾回收的內(nèi)存管理
3.1 基本原理
垃圾回收是目前主流的內(nèi)存管理機(jī)制:
- 通過一系列的稱為 “GC Root” 的對(duì)象作為起始對(duì)象
- 從 GC Root 出發(fā),進(jìn)行遍歷
- 最終將對(duì)象劃分為兩類:
- 從 GC Root 可以到達(dá)的對(duì)象
- 從 GC Root 無法到達(dá)的對(duì)象
從 GC Root 無法到達(dá)的對(duì)象被認(rèn)為是廢棄對(duì)象,可以被系統(tǒng)回收。
- 在 Python 語言中,可作為 GC Roots 的對(duì)象主要是指全局變量指向的對(duì)象。
- 從 GC Roots 出發(fā),可以到達(dá) object 1、object 2、object 3、object 4
- 從 GC Roots 出發(fā),無法到達(dá) object 5、object 6、object 7,它們被判定為可回收的對(duì)象
3.2 優(yōu)點(diǎn)和缺點(diǎn)
垃圾回收的優(yōu)點(diǎn)在于:
- 可以處理存在循環(huán)引用的對(duì)象
垃圾回收的缺點(diǎn)在于:
- 實(shí)現(xiàn)復(fù)雜
- 進(jìn)行垃圾回收時(shí),需要掃描程序中所有的對(duì)象,因此需要暫停程序的運(yùn)行。當(dāng)程序中對(duì)象數(shù)量較多時(shí),暫停程序的運(yùn)行時(shí)間過長(zhǎng),系統(tǒng)會(huì)有明顯的卡頓現(xiàn)象。
4. Python 的內(nèi)存管理機(jī)制
Python 的內(nèi)存管理采用了混合的方法:
- Python 使用引用計(jì)數(shù)來保持追蹤內(nèi)存中的對(duì)象,當(dāng)對(duì)象的引用計(jì)數(shù)為 0 時(shí),回收該對(duì)象
- Python 同時(shí)使用垃圾回收機(jī)制來回收存在有循環(huán)引用的對(duì)象
下面的例子中,演示了 Python 的內(nèi)存管理策略:
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,創(chuàng)建了一個(gè)包含循環(huán)引用的對(duì)象
- self.next 指向自身,導(dǎo)致了循環(huán)引用
- 類 Circular 的實(shí)例只能被垃圾回收機(jī)制釋放
- 類 NonCircular,創(chuàng)建了一個(gè)不包含循環(huán)引用的對(duì)象
- self.next 指向 None,沒有循環(huán)引用
- 類 NonCircular 的實(shí)例可以引用計(jì)數(shù)機(jī)制釋放
- 在方法 hybrid 中
- 在無限循環(huán)中,不斷的申請(qǐng) Circular 實(shí)例和 NonCircular 實(shí)例
通過引用計(jì)數(shù)和垃圾回收機(jī)制,內(nèi)存不會(huì)被耗盡,程序可以永遠(yuǎn)的運(yùn)行下去。