python內(nèi)存管理

python內(nèi)存管理是通過引用計數(shù)來實現(xiàn)的。當對象的引用計數(shù)為0時,會被gc回收。

為了探索對象在內(nèi)存的存儲,我們可以求助于Python的內(nèi)置函數(shù)id()。它用于返回對象的身份(identity)。其實,這里所謂的身份,就是該對象的內(nèi)存地址。判斷對象a和b的內(nèi)存地址是否一致(而不是a和b的值是否一致)可以用is來判斷。如a="good",b="good",print(a is b)//True。

a=1

b=1

print(a is b) //True

a="good"

b="good"

print(a is b) //True

a="it is a very good day"

b="it is a very good day"

print(a is b) //False

a=[]

b=[]

print(a is b) //False

由于Python緩存了整數(shù)和短字符串,因此每個對象只存有一份。比如,所有整數(shù)1的引用都指向同一對象。即使使用賦值語句,也只是創(chuàng)造了新的引用,而不是對象本身。長的字符串和其它對象可以有多個相同的對象,可以使用賦值語句創(chuàng)建出新的對象。

在Python中,每個對象都有存有指向該對象的引用總數(shù),即引用計數(shù)(reference count)。我們可以使用sys包中的getrefcount(),來查看某個對象的引用計數(shù)。需要注意的是,當使用某個引用作為參數(shù),傳遞給getrefcount()時,參數(shù)實際上創(chuàng)建了一個臨時的引用。因此,getrefcount()所得到的結(jié)果,會比期望的多1。

from sys import getrefcount

a = [1, 2, 3]

print(getrefcount(a)) //2

b = a

print(getrefcount(b))? //3

當一個對象A被另一個對象B引用時,A的引用計數(shù)將增加1。

from? sys import getrefcount

a = [1, 2, 3]

print(getrefcount(a)) //2

b = [a, a]

print(getrefcount(a))? //4

容器對象的引用可能構(gòu)成很復(fù)雜的拓撲結(jié)構(gòu)。我們可以用objgraph包來繪制其引用關(guān)系,比如:

x = [1, 2, 3]

y = [x, dict(key1=x)]

z = [y, (x, y)]

import objgraph

objgraph.show_refs([z], filename='ref_topo.png')

objgraph是Python的一個第三方包。安裝之前需要安裝xdot。

sudo apt-get install xdot

sudo pip install objgraph

兩個對象可能相互引用,從而構(gòu)成所謂的引用環(huán)(reference cycle)。如:

a = []

b = [a]

a.append(b)

即使是一個對象,只需要自己引用自己,也能構(gòu)成引用環(huán)。

a = []

a.append(a)

print(getrefcount(a)) //3

某個對象的引用計數(shù)可能減少。比如,可以使用del關(guān)鍵字刪除某個引用:

from sys import getrefcount

a = [1, 2, 3]

b = a

print(getrefcount(b))? //3

del a

print(getrefcount(b)) //2


a=[1,2,3]

print(getrefcount(a)) //2

b=[a,a]

print(getrefcount(a)) //4

print(getrefcount(b)) //2

如果某個引用指向?qū)ο驛,當這個引用被重新定向到某個其他對象B時,對象A的引用計數(shù)減少:

from sys import getrefcount

a = [1, 2, 3]

b = a

print(getrefcount(b))? //3

a = 1

print(getrefcount(b))? //2

吃太多,總會變胖,Python也是這樣。當Python中的對象越來越多,它們將占據(jù)越來越大的內(nèi)存。不過你不用太擔心Python的體形,它會乖巧的在適當?shù)臅r候“減肥”,啟動垃圾回收(garbage collection),將沒用的對象清除。在許多語言中都有垃圾回收機制,比如Java和Ruby。盡管最終目的都是塑造苗條的提醒,但不同語言的減肥方案有很大的差異.

從基本原理上,當Python的某個對象的引用計數(shù)降為0時,說明沒有任何引用指向該對象,該對象就成為要被回收的垃圾了。比如某個新建對象,它被分配給某個引用,對象的引用計數(shù)變?yōu)?。如果引用被刪除,對象的引用計數(shù)為0,那么該對象就可以被垃圾回收。比如下面的表:

a = [1, 2, 3]

del a

del a后,已經(jīng)沒有任何引用指向之前建立的[1, 2, 3]這個表。用戶不可能通過任何方式接觸或者動用這個對象。這個對象如果繼續(xù)待在內(nèi)存里,就成了不健康的脂肪。當垃圾回收啟動時,Python掃描到這個引用計數(shù)為0的對象,就將它所占據(jù)的內(nèi)存清空。

Python不能進行其它的任務(wù)。頻繁的垃圾回收將大大降低Python的工作效率。如果內(nèi)存中的對象不多,就沒有必要總啟動垃圾回收。所以,Python只會在特定條件下,自動啟動垃圾回收。當Python運行時,會記錄其中分配對象(object allocation)和取消分配對象(object deallocation)的次數(shù)。當兩者的差值高于某個閾值時,垃圾回收才會啟動。

我們可以通過gc模塊的get_threshold()方法,查看該閾值:

import gc

print(gc.get_threshold()) //返回(700, 10, 10)

返回(700, 10, 10),后面的兩個10是與分代回收相關(guān)的閾值,后面可以看到。700即是垃圾回收啟動的閾值。可以通過gc中的set_threshold()方法重新設(shè)置。

除了自動垃圾回收,也可以手動啟動垃圾回收,即使用gc.collect()。后面的兩個10都是分代回收相關(guān)的閾值,什么是分代回收呢?python采用了分代回收的策略。這一策略的基本假設(shè)是,存活時間越久的對象,越不可能在后面的程序中變成垃圾。我們的程序往往會產(chǎn)生大量的對象,許多對象很快產(chǎn)生和消失,但也有一些對象長期被使用。出于信任和效率,對于這樣一些“長壽”對象,我們相信它們的用處,所以減少在垃圾回收中掃描它們的頻率。

Python將所有的對象分為0,1,2三代。所有的新建對象都是0代對象。當某一代對象經(jīng)歷過垃圾回收,依然存活,那么它就被歸入下一代對象。垃圾回收啟動時,一定會掃描所有的0代對象。如果0代經(jīng)過一定次數(shù)垃圾回收,那么就啟動對0代和1代的掃描清理。當1代也經(jīng)歷了一定次數(shù)的垃圾回收后,那么會啟動對0,1,2,即對所有對象進行掃描。

這兩個次數(shù)即上面get_threshold()返回的(700, 10, 10)返回的兩個10。也就是說,每10次0代垃圾回收,會配合1次1代的垃圾回收;而每10次1代的垃圾回收,才會有1次的2代垃圾回收。

同樣可以用set_threshold()來調(diào)整,比如對2代對象進行更頻繁的掃描。

import gc

gc.set_threshold(700, 10, 5)

引用環(huán)的存在會給上面的垃圾回收機制帶來很大的困難。這些引用環(huán)可能構(gòu)成無法使用,但引用計數(shù)不為0的一些對象。

a = []

b = [a]

a.append(b)

del a

del b

上面我們先創(chuàng)建了兩個表對象,并引用對方,構(gòu)成一個引用環(huán)。刪除了a,b引用之后,這兩個對象不可能再從程序中調(diào)用,就沒有什么用處了。但是由于引用環(huán)的存在,這兩個對象的引用計數(shù)都沒有降到0,不會被垃圾回收。

為了回收這樣的引用環(huán),Python復(fù)制每個對象的引用計數(shù),可以記為gc_ref。假設(shè),每個對象i,該計數(shù)為gc_ref_i。Python會遍歷所有的對象i。對于每個對象i引用的對象j,將相應(yīng)的gc_ref_j減1。在結(jié)束遍歷后,gc_ref不為0的對象,和這些對象引用的對象,以及繼續(xù)更下游引用的對象,需要被保留。而其它的對象則被垃圾回收。

(1)對于每一個容器對象,設(shè)置一個gc_refs值,并將其初始化為該對象的引用計數(shù)值。

(2)對于每一個容器對象,找到所有其引用的對象,將被引用對象的gc_refs值減1.

(3)執(zhí)行完步驟2以后,所有g(shù)c_refs的值還大于0的對象都被非容器對象引用著。至少存在一個非循環(huán)引用。因此,不能釋放這些對象。將他們放入另一個集合。

(4)在步驟3中不能被釋放的對象,如果他們引用著某個對象,被引用的對象也是不能被釋放的。因此將這些對象也放入另一個集合中。

(5)此時還剩下的對象都是無法到達的對象,現(xiàn)在可以釋放這些對象了。



Python作為一種動態(tài)類型的語言,其對象和引用分離。這與曾經(jīng)的面向過程語言有很大的區(qū)別。為了有效的釋放內(nèi)存,Python內(nèi)置了垃圾回收的支持。Python采取了一種相對簡單的垃圾回收機制,即引用計數(shù),并因此需要解決孤立引用環(huán)的問題。Python與其它語言既有共通性,又有特別的地方。對該內(nèi)存管理機制的理解,是提高Python性能的重要一步。

gc module是python垃圾回收機制的接口模塊,可以通過該module啟停垃圾回收、調(diào)整回收觸發(fā)的閾值、設(shè)置調(diào)試選項。

如果沒有禁用垃圾回收,那么Python中的內(nèi)存泄露有兩種情況:要么是對象被生命周期更長的對象所引用,比如global作用域?qū)ο?;要么是循環(huán)引用中存在__del__

垃圾回收比較耗時,因此在對性能和內(nèi)存比較敏感的場景也是無法接受的,如果能解除循環(huán)引用,就可以禁用垃圾回收。

使用gc module的DEBUG選項可以很方便的定位循環(huán)引用,解除循環(huán)引用的辦法要么是手動解除,要么是使用weakref。

Python中,一切都是對象,又分為mutable和immutable對象。二者區(qū)分的標準在于是否可以原地修改,“原地“”可以理解為相同的地址??梢酝ㄟ^id()查看一個對象的“地址”,如果通過變量修改對象的值,但id沒發(fā)生變化,那么就是mutable,否則就是immutable。

判斷兩個變量是否相等(值相同)使用==, 而判斷兩個變量是否指向同一個對象使用 is。比如下面a1 a2這兩個變量指向的都是空的列表,值相同,但是不是同一個對象。

>>> a1, a2 = [], []

>>> a1 == a2

True

>>> a1 is a2

False

為了避免頻繁的申請、釋放內(nèi)存,避免大量使用的小對象的構(gòu)造析構(gòu),python有一套自己的內(nèi)存管理機制。

python會有自己的內(nèi)存緩沖池(layer2)以及對象緩沖池(layer3)。在Linux上運行過Python服務(wù)器的程序都知道,python不會立即將釋放的內(nèi)存歸還給操作系統(tǒng),這就是內(nèi)存緩沖池的原因。而對于可能被經(jīng)常使用、而且是immutable的對象,比如較小的整數(shù)、長度較短的字符串,python會緩存在layer3,避免頻繁創(chuàng)建和銷毀。

a = 1

print(getrefcount(a)) //601

從對象1的引用計數(shù)信息也可以看到,python的對象緩沖池會緩存十分常用的immutable對象,比如這里的整數(shù)1。

什么是循環(huán)引用,就是一個對象直接或者間接引用自己本身,引用鏈形成一個環(huán)。

在Python中, 所有能夠引用其他對象的對象都被稱為容器(container). 因此只有容器之間才可能形成循環(huán)引用. Python的垃圾回收機制利用了這個特點來尋找需要被釋放的對象. 為了記錄下所有的容器對象, Python將每一個 容器都鏈到了一個雙向鏈表中, 之所以使用雙向鏈表是為了方便快速的在容器集合中插入和刪除對象. 有了這個 維護了所有容器對象的雙向鏈表以后, Python在垃圾回收時使用如下步驟來尋找需要釋放的對象:

對于每一個容器對象, 設(shè)置一個gc_refs值, 并將其初始化為該對象的引用計數(shù)值.

對于每一個容器對象, 找到所有其引用的對象, 將被引用對象的gc_refs值減1.

執(zhí)行完步驟2以后所有g(shù)c_refs值還大于0的對象都被非容器對象引用著, 至少存在一個非循環(huán)引用. 因此 不能釋放這些對象, 將他們放入另一個集合.

在步驟3中不能被釋放的對象, 如果他們引用著某個對象, 被引用的對象也是不能被釋放的, 因此將這些 對象也放入另一個集合中.

此時還剩下的對象都是無法到達的對象. 現(xiàn)在可以釋放這些對象了.

關(guān)于分代回收:

除此之外, Python還將所有對象根據(jù)’生存時間’分為3代, 從0到2. 所有新創(chuàng)建的對象都分配為第0代. 當這些對象 經(jīng)過一次垃圾回收仍然存在則會被放入第1代中. 如果第1代中的對象在一次垃圾回收之后仍然存貨則被放入第2代. 對于不同代的對象Python的回收的頻率也不一樣. 可以通過gc.set_threshold(threshold0[, threshold1[, threshold2]])來定義. 當Python的垃圾回收器中新增的對象數(shù)量減去刪除的對象數(shù)量大于threshold0時, Python會對第0代對象 執(zhí)行一次垃圾回收. 每當?shù)?代被檢查的次數(shù)超過了threshold1時, 第1代對象就會被執(zhí)行一次垃圾回收. 同理每當 第1代被檢查的次數(shù)超過了threshold2時, 第2代對象也會被執(zhí)行一次垃圾回收.

為什么要分代呢,這個算法的根源來自于weak?generational?hypothesis。這個假說由兩個觀點構(gòu)成:首先是年親的對象通常死得也快,比如大量的對象都存在于local作用域;而老對象則很有可能存活更長的時間,比如全局對象,module, class。

垃圾回收的原理就如上面提示,詳細的可以看Python源碼,只不過事實上垃圾回收器還要考慮__del__,弱引用等情況,會略微復(fù)雜一些。

什么時候會觸發(fā)垃圾回收呢,有三種情況:

達到了垃圾回收的閾值,Python虛擬機自動執(zhí)行

手動調(diào)用gc.collect()

Python虛擬機退出的時候

對于垃圾回收,有兩個非常重要的術(shù)語,那就是reachable與collectable(當然還有與之對應(yīng)的unreachable與uncollectable),后文也會大量提及。

reachable是針對python對象而言,如果從根集(root)能到找到對象,那么這個對象就是reachable,與之相反就是unreachable,事實上就是只存在于循環(huán)引用中的對象,Python的垃圾回收就是針對unreachable對象。

而collectable是針對unreachable對象而言,如果這種對象能被回收,那么是collectable;如果不能被回收,即循環(huán)引用中的對象定義了__del__, 那么就是uncollectable。Python垃圾回收對uncollectable對象無能為力,會造成事實上的內(nèi)存泄露。

gc module

這里的gc(garbage collector)是Python 標準庫,該module提供了與上一節(jié)“垃圾回收”內(nèi)容相對應(yīng)的接口。通過這個module,可以開關(guān)gc、調(diào)整垃圾回收的頻率、輸出調(diào)試信息。gc模塊是很多其他模塊(比如objgraph)封裝的基礎(chǔ),在這里先介紹gc的核心API。

gc.enable(); gc.disable(); gc.isenabled()

開啟gc(默認情況下是開啟的);關(guān)閉gc;判斷gc是否開啟

gc.collection() 

執(zhí)行一次垃圾回收,不管gc是否處于開啟狀態(tài)都能使用

gc.set_threshold(t0, t1, t2); gc.get_threshold()

設(shè)置垃圾回收閾值; 獲得當前的垃圾回收閾值

注意:gc.set_threshold(0)也有禁用gc的效果

gc.get_objects()

返回所有被垃圾回收器(collector)管理的對象。這個函數(shù)非常基礎(chǔ)!只要python解釋器運行起來,就有大量的對象被collector管理,因此,該函數(shù)的調(diào)用比較耗時!

比如,命令行啟動python

>>> import gc

>>> len(gc.get_objects())

3749

gc.get_referents(*obj)

返回obj對象直接指向的對象

gc.get_referrers(*obj)

返回所有直接指向obj的對象

gc.set_debug(flags)

設(shè)置調(diào)試選項,非常有用,常用的flag組合包含以下

gc.DEBUG_COLLETABLE: 打印可以被垃圾回收器回收的對象

gc.DEBUG_UNCOLLETABLE: 打印無法被垃圾回收器回收的對象,即定義了__del__的對象

gc.DEBUG_SAVEALL:當設(shè)置了這個選項,可以被拉起回收的對象不會被真正銷毀(free),而是放到gc.garbage這個列表里面,利于在線上查找問題

內(nèi)存泄露

既然Python中通過引用計數(shù)和垃圾回收來管理內(nèi)存,那么什么情況下還會產(chǎn)生內(nèi)存泄露呢?有兩種情況:

第一是對象被另一個生命周期特別長的對象所引用,比如網(wǎng)絡(luò)服務(wù)器,可能存在一個全局的單例ConnectionManager,管理所有的連接Connection,如果當Connection理論上不再被使用的時候,沒有從ConnectionManager中刪除,那么就造成了內(nèi)存泄露。

第二是循環(huán)引用中的對象定義了__del__函數(shù),這個在《程序員必知的Python陷阱與缺陷列表》一文中有詳細介紹,簡而言之,如果定義了__del__函數(shù),那么在循環(huán)引用中Python解釋器無法判斷析構(gòu)對象的順序,因此就不做處理。

在任何環(huán)境,不管是服務(wù)器,客戶端,內(nèi)存泄露都是非常嚴重的事情。

如果是線上服務(wù)器,那么一定得有監(jiān)控,如果發(fā)現(xiàn)內(nèi)存使用率超過設(shè)置的閾值則立即報警,盡早發(fā)現(xiàn)些許還有救。當然,誰也不希望在線上修復(fù)內(nèi)存泄露,這無疑是給行駛的汽車換輪子,因此盡量在開發(fā)環(huán)境或者壓力測試環(huán)境發(fā)現(xiàn)并解決潛在的內(nèi)存泄露。在這里,發(fā)現(xiàn)問題最為關(guān)鍵,只要發(fā)現(xiàn)了問題,解決問題就非常容易了,因為按照前面的說法,出現(xiàn)內(nèi)存泄露只有兩種情況,在第一種情況下,只要在適當?shù)臅r機解除引用就可以了;在第二種情況下,要么不再使用__del__函數(shù),換一種實現(xiàn)方式,要么解決循環(huán)引用。

那么怎么查找哪里存在內(nèi)存泄露呢?武器就是兩個庫:gc、objgraph

在上面已經(jīng)介紹了gc這個模塊,理論上,通過gc模塊能夠拿到所有的被garbage collector管理的對象,也能知道對象之間的引用和被引用關(guān)系,就可以畫出對象之間完整的引用關(guān)系圖。但事實上還是比較復(fù)雜的,因為在這個過程中一不小心又會引入新的引用關(guān)系,所以,有好的輪子就直接用吧,那就是objgraph。

objgraph

objgraph的實現(xiàn)調(diào)用了gc的這幾個函數(shù):gc.get_objects(), gc.get_referents(), gc.get_referers(),然后構(gòu)造出對象之間的引用關(guān)系。objgraph的代碼和文檔都寫得比較好,建議一讀。

下面先介紹幾個十分實用的API

def count(typename)

返回該類型對象的數(shù)目,其實就是通過gc.get_objects()拿到所用的對象,然后統(tǒng)計指定類型的數(shù)目。

def by_type(typename)

返回該類型的對象列表。線上項目,可以用這個函數(shù)很方便找到一個單例對象

def show_most_common_types(limits = 10)

打印實例最多的前N(limits)個對象,這個函數(shù)非常有用。在《Python內(nèi)存優(yōu)化》一文中也提到,該函數(shù)能發(fā)現(xiàn)可以用slots進行內(nèi)存優(yōu)化的對象

def show_growth()

統(tǒng)計自上次調(diào)用以來增加得最多的對象,這個函數(shù)非常有利于發(fā)現(xiàn)潛在的內(nèi)存泄露。函數(shù)內(nèi)部調(diào)用了gc.collect(),因此即使有循環(huán)引用也不會對判斷造成影響。

另外一種更方便的方法,就是使用弱引用weakref, weakref是Python提供的標準庫,旨在解決循環(huán)引用。

weakref模塊提供了以下一些有用的API:

(1)weakref.ref(object, callback = None)

創(chuàng)建一個對object的弱引用,返回值為weakref對象,callback: 當object被刪除的時候,會調(diào)用callback函數(shù),在標準庫logging (__init__.py)中有使用范例。使用的時候要用()解引用,如果referant已經(jīng)被刪除,那么返回None。比如下面的例子

import weakref

class OBJ(object):

????def f(self):

????????print 'HELLO'


if __name__ == '__main__':

????o = OBJ()

????w = weakref.ref(o)

????w().f()

????del o

????w().f()? //拋出異常:AttributeError: ‘NoneType’ object has no attribute ‘f’。因為這個時候被引用的對象已經(jīng)被刪除了

(2)weakref.proxy(object, callback = None)

創(chuàng)建一個代理,返回值是一個weakproxy對象,callback的作用同上。使用的時候直接用 和object一樣,如果object已經(jīng)被刪除 那么拋出異常 ??ReferenceError: weakly-referenced object no longer exists。

# -*- coding: utf-8 -*-

import weakref

class OBJ(object):

????def f(self):

????????print 'HELLO'


if __name__ == '__main__':

????o = OBJ()

????w = weakref.proxy(o)

????w.f()

????del o

????w.f()

(3)weakref.WeakSet

這個是一個弱引用集合,當WeakSet中的元素被回收的時候,會自動從WeakSet中刪除。WeakSet的實現(xiàn)使用了weakref.ref,當對象加入WeakSet的時候,使用weakref.ref封裝,指定的callback函數(shù)就是從WeakSet中刪除。感興趣的話可以直接看源碼(_weakrefset.py),下面給出一個參考例子:

# -*- coding: utf-8 -*-

import weakref

class OBJ(object):

????def f(self):

????????print 'HELLO'


if __name__ == '__main__':

????o = OBJ()

????ws = weakref.WeakSet()

????ws.add(o)

????print len(ws) #??1

????del o

????print len(ws) # 0

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.元類 1.1.1類也是對象 在大多數(shù)編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在Python中這...
    TENG書閱讀 1,419評論 0 3
  • 在比較淺層次上我們通過說明如下問題來進一步深入了解python內(nèi)存管理機制:Python中到底是“傳引用”還是“傳...
    tdeblog閱讀 2,533評論 0 0
  • 參考:http://www.cnblogs.com/CBDoctor/p/3781078.html 先從較淺的層面...
    麥兜胖胖次閱讀 817評論 0 1
  • 不曾想 遇見你 不曾想 與你相約 你如同漓江的水 平靜緩慢 駛向你的遠方 追隨 是我的遠方 走在寬闊的馬路上 期望...
    艾拉夫閱讀 273評論 0 1
  • 以前沒考研的時候看到一句話 路還有一半你怎么就不走了 那時候想啊,考研無非就是再經(jīng)歷一次高三罷了,想想自己的高三也...
    桑珞閱讀 341評論 0 0

友情鏈接更多精彩內(nèi)容