1. 程式人生 > >Python記憶體管理及釋放

Python記憶體管理及釋放

python話說會自己管理記憶體,實際上,對於佔用很大記憶體的物件,並不會馬上釋放。舉例,a=range(10000*10000),會發現記憶體飆升一個多G,del a 或者a=[]都不能將記憶體降下來。。


del 可以刪除多個變數,del a,b,c,d
辦法:
import gc (garbage collector)
del a
gc.collect()

馬上記憶體就釋放了。

在IPython中用run執行程式時,都是在獨立的執行環境中執行,結束之後才將程式執行環境中的結果複製到IPython環境中,因此不會有變數被重複呼叫的問題。

如果你是指在自己的程式中想刪除所有全域性變數的話,可以自己編寫一個clear函式,通過globals()獲取全域性變數然後將其中不需要的內容刪除,例如下面的程式保留函式,類,模組,刪除所有其它全域性變數:

def clear():

    for key, value in globals().items():

        if callable(value) or value.__class__.__name__ == "module":

            continue

        del globals()[key]

不過程式中應該避免這種對全域性變數的依賴。你也可以在IPython下用此函式清空全域性變數。

以下參考:http://www.cnblogs.com/CBDoctor/p/3781078.html

先從較淺的層面來說,Python的記憶體管理機制可以從三個方面來講

(1)垃圾回收

(2)引用計數

(3)記憶體池機制

一、垃圾回收:

python不像C++,Java等語言一樣,他們可以不用事先宣告變數型別而直接對變數進行賦值。對Python語言來講,物件的型別和記憶體都是在執行時確定的。這也是為什麼我們稱Python語言為動態型別的原因(這裡我們把動態型別可以簡單的歸結為對變數記憶體地址的分配是在執行時自動判斷變數型別並對變數進行賦值)。

二、引用計數:

Python採用了類似Windows核心物件一樣的方式來對記憶體進行管理。每一個物件,都維護這一個對指向該對物件的引用的計數。如圖所示(圖片來自Python核心程式設計)

x = 3.14

y = x

我們首先建立了一個物件3.14, 然後將這個浮點數物件的引用賦值給x,因為x是第一個引用,因此,這個浮點數物件的引用計數為1. 語句y = x建立了一個指向同一個物件的引用別名y,我們發現,並沒有為Y建立一個新的物件,而是將Y也指向了x指向的浮點數物件,使其引用計數為2.

我們可以很容易就證明上述的觀點:

 

變數a 和 變數b的id一致(我們可以將id值想象為C中變數的指標).

我們援引另一個網址的圖片來說明問題:對於C語言來講,我們建立一個變數A時就會為為該變數申請一個記憶體空間,並將變數值 放入該空間中,當將該變數賦給另一變數B時會為B申請一個新的記憶體空間,並將變數值放入到B的記憶體空間中,這也是為什麼A和B的指標不一致的原因。如圖:

              

 int A = 1                       int A = 2

而Python的情況卻不一樣,實際上,Python的處理方式和Javascript有點類似,如圖所示,變數更像是附在物件上的標籤(和引用的定義類似)。當變數被繫結在一個物件上的時候,該變數的引用計數就是1,(還有另外一些情況也會導致變數引用計數的增加),系統會自動維護這些標籤,並定時掃描,當某標籤的引用計數變為0的時候,該對就會被回收。

                       

      a = 1                         a = 2                         b = a

 三、記憶體池機制

 

Python的記憶體機制以金字塔行,-1,-2層主要有作業系統進行操作,

  第0層是C中的malloc,free等記憶體分配和釋放函式進行操作;

  第1層和第2層是記憶體池,有Python的介面函式PyMem_Malloc函式實現,當物件小於256K時有該層直接分配記憶體;

  第3層是最上層,也就是我們對Python物件的直接操作;

在 C 中如果頻繁的呼叫 malloc 與 free 時,是會產生效能問題的.再加上頻繁的分配與釋放小塊的記憶體會產生記憶體碎片. Python 在這裡主要乾的工作有:

  如果請求分配的記憶體在1~256位元組之間就使用自己的記憶體管理系統,否則直接使用 malloc.

  這裡還是會呼叫 malloc 分配記憶體,但每次會分配一塊大小為256k的大塊記憶體.

  經由記憶體池登記的記憶體到最後還是會回收到記憶體池,並不會呼叫 C 的 free 釋放掉.以便下次使用.對於簡單的Python物件,例如數值、字串,元組(tuple不允許被更改)採用的是複製的方式(深拷貝?),也就是說當將另一個變數B賦值給變數A時,雖然A和B的記憶體空間仍然相同,但當A的值發生變化時,會重新給A分配空間,A和B的地址變得不再相同

而對於像字典(dict),列表(List)等,改變一個就會引起另一個的改變,也稱之為淺拷貝

附:

引用計數增加

1.物件被建立:x=4

2.另外的別人被建立:y=x

3.被作為引數傳遞給函式:foo(x)

4.作為容器物件的一個元素:a=[1,x,'33']

引用計數減少

1.一個本地引用離開了它的作用域。比如上面的foo(x)函式結束時,x指向的物件引用減1。

2.物件的別名被顯式的銷燬:del x ;或者del y

3.物件的一個別名被賦值給其他物件:x=789

4.物件從一個視窗物件中移除:myList.remove(x)

5.視窗物件本身被銷燬:del myList,或者視窗物件本身離開了作用域。

垃圾回收

1、當記憶體中有不再使用的部分時,垃圾收集器就會把他們清理掉。它會去檢查那些引用計數為0的物件,然後清除其在記憶體的空間。當然除了引用計數為0的會被清除,還有一種情況也會被垃圾收集器清掉:當兩個物件相互引用時,他們本身其他的引用已經為0了。

2、垃圾回收機制還有一個迴圈垃圾回收器, 確保釋放迴圈引用物件(a引用b, b引用a, 導致其引用計數永遠不為0)。

以下摘自vamei:http://www.cnblogs.com/vamei/p/3232088.html

在Python中,整數和短小的字元,Python都會快取這些物件,以便重複使用。當我們建立多個等於1的引用時,實際上是讓所有這些引用指向同一個物件。

a = 1
b = 1

print(id(a))
print(id(b))

上面程式返回

11246696

11246696

可見a和b實際上是指向同一個物件的兩個引用。

為了檢驗兩個引用指向同一個物件,我們可以用is關鍵字。is用於判斷兩個引用所指的物件是否相同。

複製程式碼
# True
a = 1
b = 1
print(a is b)

# True
a = "good"
b = "good"
print(a is b)

# False
a = "very good morning"
b = "very good morning"
print(a is b)

# False
a = []
b = []
print(a is b)
複製程式碼

上面的註釋為相應的執行結果。可以看到,由於Python快取了整數和短字串,因此每個物件只存有一份。比如,所有整數1的引用都指向同一物件。即使使用賦值語句,也只是創造了新的引用,而不是物件本身。長的字串和其它物件可以有多個相同的物件,可以使用賦值語句創建出新的物件。

在Python中,每個物件都有存有指向該物件的引用總數,即引用計數(reference count)。

我們可以使用sys包中的getrefcount(),來檢視某個物件的引用計數。需要注意的是,當使用某個引用作為引數,傳遞給getrefcount()時,引數實際上建立了一個臨時的引用。因此,getrefcount()所得到的結果,會比期望的多1。

複製程式碼
from sys import getrefcount

a = [1, 2, 3]
print(getrefcount(a))

b = a
print(getrefcount(b))
複製程式碼

由於上述原因,兩個getrefcount將返回2和3,而不是期望的1和2。

物件引用物件

Python的一個容器物件(container),比如表、詞典等,可以包含多個物件。實際上,容器物件中包含的並不是元素物件本身,是指向各個元素物件的引用。

我們也可以自定義一個物件,並引用其它物件:

複製程式碼
class from_obj(object):
    def __init__(self, to_obj):
        self.to_obj = to_obj

b = [1,2,3]
a = from_obj(b)
print(id(a.to_obj))
print(id(b))
複製程式碼

可以看到,a引用了物件b。

物件引用物件,是Python最基本的構成方式。即使是a = 1這一賦值方式,實際上是讓詞典的一個鍵值"a"的元素引用整數物件1。該詞典物件用於記錄所有的全域性引用。該詞典引用了整數物件1。我們可以通過內建函式globals()來檢視該詞典。

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

複製程式碼
from sys import getrefcount

a = [1, 2, 3]
print(getrefcount(a))

b = [a, a]
print(getrefcount(a))
複製程式碼

由於物件b引用了兩次a,a的引用計數增加了2。

當垃圾回收啟動時,Python掃描到這個引用計數為0的物件,就將它所佔據的記憶體清空。

然而,減肥是個昂貴而費力的事情。垃圾回收時,Python不能進行其它的任務。頻繁的垃圾回收將大大降低Python的工作效率。如果記憶體中的物件不多,就沒有必要總啟動垃圾回收。所以,Python只會在特定條件下,自動啟動垃圾回收。當Python執行時,會記錄其中分配物件(object allocation)和取消分配物件(object deallocation)的次數。當兩者的差值高於某個閾值時,垃圾回收才會啟動。

我們可以通過gc模組的get_threshold()方法,檢視該閾值:

import gc
print(gc.get_threshold())

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

我們也可以手動啟動垃圾回收,即使用gc.collect()

分代回收

Python同時採用了分代(generation)回收的策略。這一策略的基本假設是,存活時間越久的物件,越不可能在後面的程式中變成垃圾。我們的程式往往會產生大量的物件,許多物件很快產生和消失,但也有一些物件長期被使用。出於信任和效率,對於這樣一些“長壽”物件,我們相信它們的用處,所以減少在垃圾回收中掃描它們的頻率。

小傢伙要多檢查

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)