Python萬字深入內存管理講解

Python內存管理

一、對象池

1.小整數池

系統默認創建好的,等著你使用

概述:

整數在程序中的使用非常廣泛,Python為瞭優化速度,使用瞭小整數對象池,避免為整數頻繁申請和銷毀內存空間。

Python 對小整數的定義是 [-5, 256] ,這些整數對象是提前建立好的,不會被垃圾回收。

在一個 Python 的程序中,無論這個整數處於LEGB(局部變量,閉包,全局,內建模塊)中的哪個位置,所有位於這個范圍內的整數使用的都是同一個對象。

# 交互式環境下:
>>> a = 100
>>> b = 100
>>> id(a)
140720433537792
>>> id(b)
140720433537792
>>> a is b
True
>>> 

我們可以看出a,b指向同一個內存地址。

2.大整數池

大整數池:默認創建出來,池內為空的,創建一個就會往池中存儲一個

# python交互式環境
>>> a = 257
>>> b = 257
>>> id(a)
2085029722896
>>> id(b)
2085029722960
>>> a is b
False
>>> 

a , b 不是指向同一個內存地址。

python中對大於256的整數,會重新分配對象空間地址保存對象。

3.inter機制(短字符串池)

每個單詞(字符串),不夾雜空格或者其他符號,默認開啟intern機制,共享內存,靠引用計數決定是否銷毀。

>>> s1 = 'hello'
>>> s2 = 'hello'
>>> id(s1)
2178093449264
>>> id(s2)
2178093449264
>>> s1 is s2
True
>>> 

字符串s1和s2中沒有空格時,可以看出,這裡的s1與s2指向同一個內存地址。

當我們在he和llo之間加一個空格

>>> s1 = 'he llo'
>>> s2 = 'he llo'
>>> id(s1)
2278732636592
>>> id(s2)
2278732636528
>>> s1 is s2
False
>>> 

這時的字符串s1和s2就沒有指向同一個內存地址。

對於字符串來說,如果不包含空格的字符串,則不會重新分配對象空間,對於包含空格的字符串則會重新分配對象空間。

二、垃圾回收

概述:

python采用的是引用計數機制為主,隔代回收和標記清除機制為輔的策略

概述:
現在的高級語言如java,c\# 等,都采用瞭垃圾收集機制,而不再是c,c++裡用戶自己管理維護內存的方式。

自己管理 內存極其自由, 可以任意申請內存,但如同一把雙刃劍,為大量內存泄露,懸空指針等bug埋下隱患。 

python裡也同java一樣采用瞭垃圾收集機制,不過不一樣的是: 
python采用的是引用計數機制為主,隔代回收機制為輔的策略

2.1.引用計數

在Python中,每個對象都有指向該對象的引用總數—引用計數 

查看對象的引用計數:sys.getrefcount() 

註意:
    當使用某個引用作為參數,傳遞給getrefcount()時,參數實際上創建瞭一個臨時的引用。
    因此, getrefcount()所得到的結果,會比期望的多1。

2.1.1 引用計數增加

a、對象被創建

b、另外變量也指向當前對象

c、作為容器對象的一個元素

d、作為參數提供給函數:test(x)

2.1.2 引用計數減少

a、變量被顯式的銷毀

b、對象的另外一個變量重新賦值

c、從容器中移除

d、函數被執行完畢

看代碼:

# -*- coding: utf-8 -*-
import sys
class Test(object):
    def __init__(self):
        print('當前對象已經被創建,占用的內存地址為:%s' % hex(id(self)))
a = Test()
print('當前對象的引用計數為:%s' % sys.getrefcount(a))  # 2
b = a
print('當前對象的引用計數為:%s' % sys.getrefcount(a))  # 3
list1 = []
list1.append(a)
print('當前對象的引用計數為:%s' % sys.getrefcount(a))  # 4
del b
print('當前對象的引用計數為:%s' % sys.getrefcount(a))  # 3
list1.remove(a)
print('當前對象的引用計數為:%s' % sys.getrefcount(a))  # 2
del a
print('當前對象的引用計數為:%s' % sys.getrefcount(a))  # 報錯
'''
Traceback (most recent call last):
  File "E:/Python Project/Python 高級編程/內存管理/垃圾收集.py", line 30, in <module>
    print('當前對象的引用計數為:%s' % sys.getrefcount(a))
NameError: name 'a' is not defined
'''

當Python的某個對象的引用計數降為0時,說明沒有任何引用指向該對象,該對象就成為要被回收的垃圾。比如某個新建對象,被分配給某個引用,對象的引用計數變為1。如 為0,那麼該對象就可以被垃圾回收。

2.2.標記清除

標記清除(Mark—Sweep)算法是一種基於追蹤回收(tracing GC)技術實現的垃圾回收算法。

它分為兩個階段:

第一階段是標記階段,GC會把所有的活動對象打上標記

第二階段是把那些沒有標記的對象非活動對象進行回收。

對象之間通過引用(指針)連在一起,構成一個有向圖,對象構成這個有向圖的節點,而引用關系構成這個有向圖的邊。從根對象(root object)出發,沿著有向邊遍歷對象,可達的(reachable)對象標記為活動對象,不可達的對象就是要被清除的非活動對象。根對象就是全局變量、調用棧、寄存器。

>

在上圖中,可以從程序變量直接訪問塊1,並且可以間接訪問塊2和3。程序無法訪問塊4和5。第一步將標記塊1,並記住塊2和3以供稍後處理。第二步將標記塊2,第三步將標記塊3,但不記得塊2,因為它已被標記。掃描階段將忽略塊1,2和3,因為它們已被標記,但會回收塊4和5。

標記清除算法作為Python的輔助垃圾收集技術,主要處理的是一些容器對象,比如list、dict、tuple等,因為對於字符串、數值對象是不可能造成循環引用問題。Python使用一個雙向鏈表將這些容器對象組織起來。不過,這種簡單粗暴的標記清除算法也有明顯的缺點:清除非活動的對象前它必須順序掃描整個堆內存,哪怕隻剩下小部分活動對象也要掃描所有對象。

2.3.分代回收

因為, 標記和清除的過程效率不高。清除非活動的對象前它必須順序掃描整個堆內存,哪怕隻剩下小部分活動對象也要掃描所有對象。還有一個問題就是:什麼時候掃描去檢測循環引用?

為瞭解決上述的問題,python又引入瞭分代回收。分代回收解決瞭標記清楚時什麼時候掃描的問題,並且將掃描的對象分成瞭3級,以及降低掃描的工作量,提高效率。

  • 0代: 0代中對象個數達到700個,掃描一次。
  • 1代: 0代掃描10次,則1代掃描1次。
  • 2代: 1代掃描10次,則2代掃描1次。

隔代回收是用來解決交叉引用(循環引用),並增加數據回收的效率. 原理:通過對象存在的時間不同,采用不同的算法來 回收垃圾.

形象的比喻, 三個鏈表,零代鏈表上的對象(新創建的對象都加入到零代鏈表),引用數都是一,每增加一個指針,引用加一,隨後 python會檢測列表中的互相引用的對象,根據規則減掉其引用計數.

GC算法對鏈表一的引用減一,引用為0的,清除,不為0的到鏈表二,鏈表二也執行GC算法,鏈表三一樣. 存在時間越長的 數據,越是有用的數據

2.3.1 分代回收觸發時機?(GC閾值)

隨著你的程序運行,Python解釋器保持對新創建的對象,以及因為引用計數為零而被釋放掉的對象的追蹤。

從理論上說,這兩個值應該保持一致,因為程序新建的每個對象都應該最終被釋放掉。當然,事實並非如此。因為循環 引用的原因,從而被分配對象的計數值與被釋放對象的計數值之間的差異在逐漸增長。一旦這個差異累計超過某個閾 值,則Python的收集機制就啟動瞭,並且觸發上邊所說到的零代算法,釋放“浮動的垃圾”,並且將剩下的對象移動到 一代列表。

隨著時間的推移,程序所使用的對象逐漸從零代列表移動到一代列表。而Python對於一代列表中對象的處理遵循同樣的 方法,一旦被分配計數值 與被釋放計數值累計到達一定閾值,Python會將剩下的活躍對象移動到二代列表。

通過這種方法,你的代碼所長期使用 的對象,那些你的代碼持續訪問的活躍對象,會從零代鏈表轉移到一代再轉移到二代。

通過不同的閾值設置,Python可 以在不同的時間間隔處理這些對象。

Python處理零代最為頻繁,其次是一代然後才是二代。

2.3.2 查看引用計數(gc模塊的使用)

# 引入gc模塊
import gc 
# 常用函數: 
gc.get_count() 
# 獲取當前自動執行垃圾回收的計數器,返回一個長度為3的列表
gc.get_threshold() 
# 獲取gc模塊中自動執行垃圾回收的頻率 
gc.set_threshold(threshold0[,threshold1,threshold2]) 
# 設置自動執行垃圾回收的頻率 
gc.disable() 
# python3默認開啟gc機制,可以使用該方法手動關閉gc機制 
gc.collect() 
# 手動調用垃圾回收機制回收垃圾

內存管理是使用計算機必不可少的一部分,無論好壞,Python幾乎會在後臺處理一切內存管理的問題。Python抽象出許多使用計算機的嚴格細節,這讓我們在更高層次進行開發,而不用擔心所有字節的存儲方式和位置。

# -*- coding: utf-8 -*-
import gc
import sys
import time
class Test(object):
    def __init__(self):
        print('當前對象已經被創建,占用的內存地址為:%s' % hex(id(self)))
    def __del__(self):
        print('當前對象馬上被系統GC回收')
# gc.disable()  # 不啟用GC,在python3中默認啟用
while True:
    a = Test()
    b = Test()
    a.pro = b  # a 和 b之間相互引用
    b.pro = a
    del a
    del b
    print(gc.get_threshold())  # 打印隔代回收的閾值
    print(gc.get_count())  # 打印GC需要回收的對象
    time.sleep(0.2)  # 休眠0.2秒方便查看

終端輸出:

三、怎麼優化內存管理

1.手動垃圾回收

先調用del a ; 再調用gc.collect()即可手動啟動GC

2.調高垃圾回收閾值

gc.set_threshold 設置垃圾回收閾值(收集頻率)。

將 threshold 設為零會禁用回收。

3.避免循環引用

四、總結

python采用的是引用計數機制為主,標記-清除和分代回收(隔代回收)兩種機制為輔的策略

到此這篇關於Python萬字深入內存管理講解的文章就介紹到這瞭,更多相關Python內存管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: