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