Python記憶體管理

來源:互聯網
上載者:User

標籤:拓撲   on()   預設   connect   isa   動態類型   動態   分代   error   

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記憶體管理

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.