python的內存管理算法與優化

python的內存管理算法與優化

前期準備

  1. 我們可以用python的gc模塊控制python的內存管理和回收
    • gc.disable()# 暫停自動垃圾回收
    • gc.collect()# 執行完整的垃圾回收,返回無法到達的對象的數量
    • gc.set_threshold()# 設置垃圾回收的閾值
    • gc.set_debug()# 設置垃圾回收的調試標記. 調試信息會被寫入std.err.
  2. sys跟objgraph庫

python內存管理算法

python的內存管理機制有兩種:引用計數和分代垃圾回收

引用次數
  1. 引用計數+1的情況
    對象被創建 a=‘123’
    對象被引用 b=a
    對象被當作參數傳入函數 fun(a)
    對象最爲元素存儲到容器中 c={a:’1’}
  2. 引用計數-1的情況
    對象的別名被顯式銷燬 del b
    對象的別名被賦予其它值 b=1
    對象離開它的作用域,比如函數執行完畢後,函數裏面的局部變量的引用計數-1
    對象從容器中刪除,或者所在的容器被銷燬 del c
  3. 引用計數的優點
    高效
    回收內存的時間是分佈的,引用計數爲0馬上回收,不會給系統造成停頓
    對象生命週期明確
    容易實現
  4. 引用計數缺點
    額外空間維護引用計數
    無法解決循環引用的情況
    循環引用的例子
a=[1]
b=[2]
a.append(b)
b.append(a)


del a
del b

#del a del b只是把引用計數-1,del後ab原來所指的對象的引用計數爲1 無法進行資源回收,但也無法訪問


  1. 查看引用計數的方法
    sys.getrefcount()
    objgraph.count()
垃圾回收機制

python的垃圾回收機制就是爲了解決循環引用的問題
python的垃圾回收機制分爲mark-sweep算法和分代(generational)算法

  1. 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找出來的部分進行回收

  1. 分代(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),所以消耗會很大,我們可以在程序層面進行一些調優

調優方式
  1. 手動垃圾回收
  • 關閉自動回收gc.disable()
  • 合適的時候用gc.collect()觸發垃圾回收,比如打遊戲過程中不進行垃圾回收,在用戶等待或遊戲結算的時候再出發垃圾回收
  1. 提高垃圾回收閾值
    通過gc.set_threshold()設置回收閾值,減少垃圾回收的次數

  2. 避免循環引用
    整個垃圾回收機制都是爲了解決循環引用的問題,如果代碼能保證沒有循環引用問題,則可以直接關閉垃圾回收

常見手段

  • 手動解循環引用
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章