萬盛學電腦網

 萬盛學電腦網 >> 網絡編程 >> 編程語言綜合 >> 分析Python中的內存洩漏

分析Python中的內存洩漏

   分析Python中的內存洩漏

  引子

  之前一直盲目的認為 Python 不會存在內存洩露, 但是眼看著上線的項目隨著運行時間的增長 而越來越大的內存占用, 我意識到我寫的程序在發生內存洩露, 之前 debug 過 logging 模塊導致的內存洩露.

  目前看來, 還有別的地方引起的內存洩露. 經過一天的奮戰, 終於找到了內存洩露的地方, 目前項目 跑了很長時間, 在業務量較小的時候內存還是能回到剛啟動的時候的內存占用.

  什麼情況下不用這麼麻煩

  如果你的程序只是跑一下就退出大可不必大費周章的去查找是否有內存洩露, 因為 Python 在退出時 會釋放它所分配的所有內存, 如果你的程序需要連續跑很長時間那麼就要仔細的查找是否 產生了內存洩露.

  場景

  如何產生的內存洩露呢, 項目是一個 TCP server, 每當有連接過來時都會創建一個連接實例來進行 管理, 每次斷開時連接實例還被占用並沒有釋放. 沒有被釋放的原因肯定是因為有某個地方對連接 實例的引用沒有釋放, 所以隨著時間的推移, 連接創建分配內存, 連接斷開並沒有釋放掉內存, 所以 就會產生內存洩露.

  調試方法

  由於不知道具體是哪裡引起的內存洩露, 所以要耐心的一點點調試.

  由於知道了斷開連接時沒有釋放, 所以我就不停的模擬創建連接然後發送一些包後斷開連接, 然後通過下面一行 shell 來觀察內存占用情況:

  PID=50662;while true; do; ps aux | grep $PID | grep -v grep | awk '{print $5" "$6}' >> t; sleep 1; done

  如果在增長了一定的量後保持住就說明已經沒有產生洩露.

  同時可以在對象該釋放的時候查看對象的引用計數, 通過 sys.getrefcount(obj). 如果引用計數變為了 2 則說明該對象在跳出命名空間後就會被正確回收.

  產生原因

  項目中兩種情況導致對象沒有被正確回收:

  被退出才回收的對象引用

  交叉引用

  被退出才回收的對象引用

  為了追蹤連接所以把連接對象同時放在一個列表裡, 而這個列表只有在程序退出時才會被回收, 如果不正確處理, 那麼分配的對象將也會只在程序退出時才會被回收.

  全局變量和類變量都只會在程序退出的時候才會被回收:

  ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 _CONNECTIONS = []   # ... class Connection(object): def __init__(self, sock, address) pass   def server_loop(): # ... sock, address = server_sock.accept() connection = Connection(sock, address) _CONNECTIONS.append(connection) # ... sock.close()

  上面把所有建立的連接都放在全局變量 _CONNECTIONS 裡, 如果在關閉的時候不從這個列表 裡取出(減少引用)則 connection 對象就不會被回收, 則每建立一次連接就會有個連接對象和連接 對象引用的對象不會被回收.

  如果把對象放在一個類屬性裡也是一樣的, 因為類對象在程序一開始就分配, 並在程序退出時才被回收.

  解決辦法就是在退出時從列表(或其他對象)裡解除對對象的引用(刪除)

  ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 _CONNECTIONS = []   # ... class Connection(object): def __init__(self, sock, address) pass   def server_loop(): # ... sock, address = server_sock.accept() connection = Connection(sock, address) _CONNECTIONS.append(connection) try: # ... sock.close() finally: _CONNECTIONS.remove(connection) # XXX

  交叉引用

  有時候我們為對象分配一個實例屬性時需要將自己本身賦值給實例屬性, 作為實例屬性的實例屬性, 說著很拗口, 看一下代碼:

  ?

1 2 3 4 5 6 7 8 class ConnectionHandler(object): def __init__(self, connection): self._conn = connection     class Connection(object): def __init__(self, sock, address) self._conn_handler = ConnectionHandler(self) # XXX

  上面的代碼就會產生交叉引用, 交叉引用會讓解釋器困惑, 從而之後只能靠2代和3代回收, 這個過程可能會很慢.

  解決這種問題的方法就是使用 弱引用

  ?

1 2 3 4 5 6 7 8 9 10 import weakref   class ConnectionHandler(object): def __init__(self, connection): self._conn = connection     class Connection(object): def __init__(self, sock, address) self._conn_handler = ConnectionHandler(weakref.proxy(self)) # XXX
copyright © 萬盛學電腦網 all rights reserved