1. 程式人生 > >Python內存管理

Python內存管理

管理機 十分 全局 建立 頻繁 核心 shu enabled 銷毀

python內存管理是通過引用計數來實現的。當對象的引用計數為0時,會被gc回收。

為了探索對象在內存的存儲,我們可以求助於Python的內置函數id()。它用於返回對象的身份(identity)。其實,這裏所謂的身份,就是該對象的內存地址。判斷對象a和b的內存地址是否一致(而不是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緩存了整數和短字符串,因此每個對象只存有一份。比如,所有整數1的引用都指向同一對象。即使使用賦值語句,也只是創造了新的引用,而不是對象本身。長的字符串和其它對象可以有多個相同的對象,可以使用賦值語句創建出新的對象。

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

from sys import getrefcount

a = [1, 2, 3]

print(getrefcount(a)) //2

b = a

print(getrefcount(b)) //3

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

from sys import getrefcount

a = [1, 2, 3]

print(getrefcount(a)) //2

b = [a, a]

print(getrefcount(a)) //4

容器對象的引用可能構成很復雜的拓撲結構。我們可以用objgraph包來繪制其引用關系,比如:

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

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

a = []

b = [a]

a.append(b)

即使是一個對象,只需要自己引用自己,也能構成引用環。

a = []

a.append(a)

print(getrefcount(a)) //3

某個對象的引用計數可能減少。比如,可以使用del關鍵字刪除某個引用:

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

如果某個引用指向對象A,當這個引用被重新定向到某個其他對象B時,對象A的引用計數減少:

from sys import getrefcount

a = [1, 2, 3]

b = a

print(getrefcount(b)) //3

a = 1

print(getrefcount(b)) //2

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

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

a = [1, 2, 3]

del a

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

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

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

import gc

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

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

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

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

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

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

import gc

gc.set_threshold(700, 10, 5)

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

a = []

b = [a]

a.append(b)

del a

del b

上面我們先創建了兩個表對象,並引用對方,構成一個引用環。刪除了a,b引用之後,這兩個對象不可能再從程序中調用,就沒有什麽用處了。但是由於引用環的存在,這兩個對象的引用計數都沒有降到0,不會被垃圾回收。

為了回收這樣的引用環,Python復制每個對象的引用計數,可以記為gc_ref。假設,每個對象i,該計數為gc_ref_i。Python會遍歷所有的對象i。對於每個對象i引用的對象j,將相應的gc_ref_j減1。在結束遍歷後,gc_ref不為0的對象,和這些對象引用的對象,以及繼續更下遊引用的對象,需要被保留。而其它的對象則被垃圾回收。

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

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

(3)執行完步驟2以後,所有gc_refs的值還大於0的對象都被非容器對象引用著。至少存在一個非循環引用。因此,不能釋放這些對象。將他們放入另一個集合。

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

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

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

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

如果沒有禁用垃圾回收,那麽Python中的內存泄露有兩種情況:要麽是對象被生命周期更長的對象所引用,比如global作用域對象;要麽是循環引用中存在__del__

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

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

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

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

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

>>> a1 == a2

True

>>> a1 is a2

False

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

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

a = 1

print(getrefcount(a)) //601

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

什麽是循環引用,就是一個對象直接或者間接引用自己本身,引用鏈形成一個環。

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

對於每一個容器對象, 設置一個gc_refs值, 並將其初始化為該對象的引用計數值.

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

執行完步驟2以後所有gc_refs值還大於0的對象都被非容器對象引用著, 至少存在一個非循環引用. 因此 不能釋放這些對象, 將他們放入另一個集合.

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

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

關於分代回收:

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

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

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

什麽時候會觸發垃圾回收呢,有三種情況:

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

手動調用gc.collect()

Python虛擬機退出的時候

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

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

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

gc module

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

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

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

gc.collection() 

執行一次垃圾回收,不管gc是否處於開啟狀態都能使用

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

設置垃圾回收閾值; 獲得當前的垃圾回收閾值

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

gc.get_objects()

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

比如,命令行啟動python

>>> import gc

>>> len(gc.get_objects())

3749

gc.get_referents(*obj)

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

gc.get_referrers(*obj)

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

gc.set_debug(flags)

設置調試選項,非常有用,常用的flag組合包含以下

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

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

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

內存泄露

既然Python中通過引用計數和垃圾回收來管理內存,那麽什麽情況下還會產生內存泄露呢?有兩種情況:

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

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

在任何環境,不管是服務器,客戶端,內存泄露都是非常嚴重的事情。

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

那麽怎麽查找哪裏存在內存泄露呢?武器就是兩個庫:gc、objgraph

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

objgraph

objgraph的實現調用了gc的這幾個函數:gc.get_objects(), gc.get_referents(), gc.get_referers(),然後構造出對象之間的引用關系。objgraph的代碼和文檔都寫得比較好,建議一讀。

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

def count(typename)

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

def by_type(typename)

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

def show_most_common_types(limits = 10)

打印實例最多的前N(limits)個對象,這個函數非常有用。在《Python內存優化》一文中也提到,該函數能發現可以用slots進行內存優化的對象

def show_growth()

統計自上次調用以來增加得最多的對象,這個函數非常有利於發現潛在的內存泄露。函數內部調用了gc.collect(),因此即使有循環引用也不會對判斷造成影響。

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

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

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

創建一個對object的弱引用,返回值為weakref對象,callback: 當object被刪除的時候,會調用callback函數,在標準庫logging (__init__.py)中有使用範例。使用的時候要用()解引用,如果referant已經被刪除,那麽返回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’。因為這個時候被引用的對象已經被刪除了

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

創建一個代理,返回值是一個weakproxy對象,callback的作用同上。使用的時候直接用 和object一樣,如果object已經被刪除 那麽拋出異常 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的實現使用了weakref.ref,當對象加入WeakSet的時候,使用weakref.ref封裝,指定的callback函數就是從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

Python內存管理