1. 程式人生 > >粗略分析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 則說明該物件在跳出名稱空間後就會被正確回收.
產生原因

專案中兩種情況導致物件沒有被正確回收:

  •     被退出才回收的物件引用
  •     交叉引用

被退出才回收的物件引用

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

全域性變數和類變數都只會在程式退出的時候才會被回收:

_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 物件就不會被回收, 則每建立一次連線就會有個連線物件和連線 物件引用的物件不會被回收.

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

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

_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

交叉引用

有時候我們為物件分配一個例項屬性時需要將自己本身賦值給例項屬性, 作為例項屬性的例項屬性, 說著很拗口, 看一下程式碼:

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代回收, 這個過程可能會很慢.

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

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