python的內存管理算法與優化
前期準備
- 我們可以用python的gc模塊控制python的內存管理和回收
- gc.disable()# 暫停自動垃圾回收
- gc.collect()# 執行完整的垃圾回收,返回無法到達的對象的數量
- gc.set_threshold()# 設置垃圾回收的閾值
- gc.set_debug()# 設置垃圾回收的調試標記. 調試信息會被寫入std.err.
- sys跟objgraph庫
python內存管理算法
python的內存管理機制有兩種:引用計數和分代垃圾回收
引用次數
- 引用計數+1的情況
對象被創建 a=‘123’
對象被引用 b=a
對象被當作參數傳入函數 fun(a)
對象最爲元素存儲到容器中 c={a:’1’} - 引用計數-1的情況
對象的別名被顯式銷燬 del b
對象的別名被賦予其它值 b=1
對象離開它的作用域,比如函數執行完畢後,函數裏面的局部變量的引用計數-1
對象從容器中刪除,或者所在的容器被銷燬 del c - 引用計數的優點
高效
回收內存的時間是分佈的,引用計數爲0馬上回收,不會給系統造成停頓
對象生命週期明確
容易實現 - 引用計數缺點
額外空間維護引用計數
無法解決循環引用的情況
循環引用的例子
a=[1]
b=[2]
a.append(b)
b.append(a)
del a
del b
#del a del b只是把引用計數-1,del後ab原來所指的對象的引用計數爲1 無法進行資源回收,但也無法訪問
- 查看引用計數的方法
sys.getrefcount()
objgraph.count()
垃圾回收機制
python的垃圾回收機制就是爲了解決循環引用的問題
python的垃圾回收機制分爲mark-sweep算法和分代(generational)算法
- mark-sweep算法
分爲mark(標記)和sweep(清除)兩部分
python中所有能夠引用其它對象的對象都叫做container(容器),只有container之間纔會出現循環引用
把所有的容器都放到一個雙向鏈表中,使用雙向鏈表是爲了方便快速插入刪除對象
mark部分具體的操作如下
- 每個容器設置一個gc_ref,並初始化爲該容器的引用計數值ob_ref
- 對每個容器,找到它引用的所有對象,將被引用對象的gc_ref-1
- 對所有容器執行完上述操作後,所有gc_ref不爲0的容器則還存在被引用的情況,不能銷燬,把他們放到另一個集合A
- 上一個操作中的集合A中,他們所引用的對象也是不能釋放的,也放進集合A中
- 剩下的不在集合A裏面的則可以進行回收
需要回收的內存就是存在循環引用的容器,循環引用的容器集合會成爲一個孤島,外部沒有辦法訪問到,mark部分的原理其實就是模擬了一次容器自身的釋放,這樣就可以打破循環引用的容器集合中互相依賴的情況
sweep部分可以略過,就是對mark找出來的部分進行回收
- 分代(generational)算法
python根據容器的活躍程度把容器分爲三代:0代、1代、2代,每一代都是一個由雙向鏈表實現的容器集合
上述的mark-sweep算法其實並非每次都對所有容器都進行標記清除,而是每次對同一代的容器進行標記清除
給容器分代的原因—弱代假說
- 弱代假說的觀點:年輕的對象更快銷燬,年老的對象可能存活更長時間,比如局部變量跟全局變量的對比
- 如果不用分代算法,每次都對所有容器進行mark-sweep,實際上有一部分容器還沒到達銷燬時間,我們不希望這部分容器被頻繁地執行算法,所以有了分代算法
分代算法的過程跟觸發每一代的規則
- 每當容器被創建時,python把它加入0代鏈表中
- 0代:當被分配的對象的數量減去被釋放對象後的差值大於設置的threshold0時,啓動0代中的mark-sweep算法,產生的不被銷燬的容器集合併入1代鏈表中
- 1代:當0代啓動mark-sweep算法的次數大於設置的threshold1時,啓動1代中的mark-sweep算法,產生的不被銷燬的容器集合併入2代鏈表中
- 2代:當1代啓動mark-sweep算法的次數大於設置的threshold2時,啓動2代中的mark-sweep算法
通過gc.set_threshold()可以設置threshold0、threshold1、threshold2的值
python內存管理優化
每一次python進行垃圾回收,都要對所有的容器進行兩次遍歷(第一次設置gc_ref值,第二次讓gc_ref-1),所以消耗會很大,我們可以在程序層面進行一些調優
調優方式
- 手動垃圾回收
- 關閉自動回收gc.disable()
- 合適的時候用gc.collect()觸發垃圾回收,比如打遊戲過程中不進行垃圾回收,在用戶等待或遊戲結算的時候再出發垃圾回收
-
提高垃圾回收閾值
通過gc.set_threshold()設置回收閾值,減少垃圾回收的次數 -
避免循環引用
整個垃圾回收機制都是爲了解決循環引用的問題,如果代碼能保證沒有循環引用問題,則可以直接關閉垃圾回收
常見手段
- 手動解循環引用
class A(object):
def __init__(self):
self.child = None
def destroy(self):
self.child = None
class B(object):
def __init__(self):
self.parent = None
def destroy(self):
self.parent = None
def test3():
a = A()
b = B()
a.child = b
b.parent = a
a.destroy()
b.destroy()
test3()
print 'Object count of A:', objgraph.count('A’) #0
print 'Object count of B:', objgraph.count('B’) #0
- 使用弱引用,python自帶的弱引用庫weakref 弱引用相關參考:https://yuerblog.cc/2018/08/28/python-weakref-real-usage/
def test4():
a = A()
b = B()
a.child = weakref.ref(b)
b.parent = weakref.ref(a)
test4()
print 'Object count of A:', objgraph.count('A’) #0
print 'Object count of B:', objgraph.count('B’) #0
內存泄露
有了引用計數和垃圾回收,python仍然有可能發生內存泄露,發生的情況如下
- 對象被另一個生命週期特別長的對象所引用,比如網絡服務器,可能存在一個全局的單例ConnectionManager,管理所有的連接Connection,如果當Connection理論上不再被使用的時候,沒有從ConnectionManager中刪除,那麼就造成了內存泄露。
- 循環引用的對象中定義了__del__函數,如果定義了這個函數,python無法判斷析構對象的順序,因此會不做處理
參考
http://www.doc88.com/p-78747715867.html
http://kkpattern.github.io/2015/06/20/python-memory-optimization-zh.html
https://blog.csdn.net/xiongchengluo1129/article/details/80462651
https://www.cnblogs.com/xybaby/p/7491656.html