一、python的內存機制
python中的內存機制 如下所示:
_____ ______ ______ ________
[ int ] [ dict ] [ list ] ... [ string ] Python core |
+3 | <----- Object-specific memory -----> | <-- Non-object memory --> |
_______________________________ | |
[ Python's object allocator ] | |
+2 | ####### Object memory ####### | <------ Internal buffers ------> |
______________________________________________________________ |
[ Python's raw memory allocator (PyMem_ API) ] |
+1 | <----- Python memory (under PyMem manager's control) ------> | |
__________________________________________________________________
[ Underlying general-purpose allocator (ex: C library malloc) ]
0 | <------ Virtual memory allocated for the python process -------> |
=========================================================================
_______________________________________________________________________
[ OS-specific Virtual Memory Manager (VMM) ]
-1 | <--- Kernel dynamic storage allocation & management (page-based) ---> |
__________________________________ __________________________________
[ ] [ ]
-2 | <-- Physical memory: ROM/RAM --> | | <-- Secondary storage (swap) --> |
解釋:
-
-1,-2層主要由操作系統進行操作。
-
第0層是由C語言中的malloc,free等內存分配和釋放函數進行內存操作
-
第1層則是在第0層的基礎之上對其提供的接口進行了統一的封裝。
這是因為:雖然不同的操作系統都提供標准定義的內存管理接口,但是對於某些特殊的情況,不同的操作系統都有不同的行為,比如說調用malloc(0),有的操作系統會返回NULL,表示內存申請失敗;然而有的操作系統會返回一個貌似正常的指針,但是這個指針所指的內存並不是有效的。為了廣泛的移植性,Python必須保證相同的語義一定代表相同的運行行為。
- 第2層是內存池,由Python的接口函數PyMem_Malloc函數實現。
Python為了避免頻繁的申請和刪除內存所造成系統切換於用戶態和核心態的開銷,從而引入了內存池機制,專門用來管理小內存的申請和釋放。當對象小於256K時有該層直接在內存池中分配內存,大於則退化由低層來進行分配,如由malloc函數進行分配。整個小塊內存的內存池可以視為一個層次結構,其一共分為4個層次,從下之上分別是block、pool、arena和內存池。需要說明的是:block、pool和area都是代碼中可以找到的實體,而最頂層的內存池只是一個概念上的東西,表示Python對於整個小塊內存分配和釋放行為的內存管理機制。
(1) block:最小的內存單元,大小為8的整數倍。有很多種類的block,不同種類的block都有不同的內存大小,申請內存的時候只需要找到適合自身大小的block即可,當然申請的內存也是存在一個上限,如果超過這個上限,則退化到使用最底層的malloc進行申請。
(2) pool:一個pool管理着一堆有固定大小的內存塊,其大小通常為一個系統內存頁的大小。
(3) arena:多個pool組合成一個arena。
(4) 內存池:一個整體的概念。
python內存池設計參考文章:https://blog.csdn.net/zhzhl202/article/details/7547445
- 第3層是最上層,也就是我們對Python對象的直接操作。直接面向用戶,它提供給我們int,list,string,dict等方法。
二、python的垃圾回收
Python中的垃圾回收是以引用計數為主,分代收集為輔。引用計數的缺陷是循環引用的問題,為了解決循環引用的問題,又有了標記 - 清除技術。
在Python中,如果一個對象的引用數為0,Python虛擬機就會回收這個對象的內存。
1. 引用計數
1.1 原理:
當一個對象的引用被創建或者復制時,對象的引用計數加1;當一個對象的引用被銷毀時,對象的引用計數減1,當對象的引用計數減少為0時,就意味着對象已經再沒有被使用了,可以將其內存釋放掉。
1.2 優缺點:
-
優點:引用計數有一個很大的優點,即實時性,任何內存,一旦沒有指向它的引用,就會被立即回收,而其他的垃圾收集技術必須在某種特殊條件下才能進行無效內存的回收。
-
缺點:但是它也有弱點,引用計數機制所帶來的維護引用計數的額外操作與Python運行中所進行的內存分配和釋放,引用賦值的次數是成正比的,這顯然比其它那些垃圾收集技術所帶來的額外操作只是與待回收的內存數量有關的效率要低。同時,引用技術還存在另外一個很大的問題 — 循環引用,因為對象之間相互引用,每個對象的引用都不會為0,所以這些對象所占用的內存始終都不會被釋放掉。(這也標記-清除計數存在的意義。)
1.3 一個例子:
# encoding=utf-8
class ClassA():
def __init__(self):
print('object born id:%s' % str(hex(id(self)))) # hex()將10進制整數轉換成16進制,以字符串形式表示。
def __del__(self):
print('object del id:%s' % str(hex(id(self))))
def func():
c1 = ClassA()
del c1
func()
程序輸出:
object born id:0x194921515f8
object del id:0x194921515f8
c1=ClassA()會創建一個對象,放在0x194921515f8內存中,c1變量指向這個內存,這時候這個內存的引用計數是1。del c1后,c1變量不再指向0x194921515f8內存,所以這塊內存的引用計數減一,等於0,所以就銷毀了這個對象,然后釋放內存。
1.4 兩種情況:
** 1.4.1 導致引用計數+1的情況:**
- 對象被創建,例如a=3,b=ClassA()
- 對象被引用,例如b=a
- 對象被作為參數,傳入到一個函數中,例如func(a)。實際上在函數內部可以看到引用計數是+2
- 對象作為一個元素,存儲在容器中,例如list1=[a,a]
** 1.4.2 導致引用計數-1的情況:**
- 對象的別名被顯式銷毀,例如del a
- 對象的別名被賦予新的對象,例如a=24
- 一個對象離開它的作用域,例如f函數執行完畢時,func函數中的局部變量(全局變量不會)
- 對象所在的容器被銷毀,或從容器中刪除對象
1.5 一個特殊的實例:
def f1(n):
print("in function:", sys.getrefcount(n) - 1)
x = 22
print("init x:", sys.getrefcount(x) - 1)
a = 22
print("after a:", sys.getrefcount(x) - 1)
b = a
print("after b:", sys.getrefcount(x) - 1)
f1(x)
print("after function:", sys.getrefcount(22) - 1)
運行結果:
init x: 12
after a: 13
after b: 14
in function: 16
after function: 14
可以看到,調用函數后再函數內部引用計數是+2,原因是: 多的那一個引用是函數棧保存了入參對形參的引用,這導致計數+2。
這個結論 參考文章:https://www.cnblogs.com/hellcat/p/10450785.html
2. 標記清除
標記-清除只關注那些可能會產生循環引用的對象,顯然,像是int、stringt這些不可變對象是不可能產生循環引用的,因為它們內部不可能持有其它對象的引用。在Python中, 所有能夠引用其他對象的對象都被稱為容器(container)。Python中的循環引用總是發生在container對象之間,也就是能夠在內部持有其它對象的對象,比如list、dict、class等等。
前面提到過,循環引用使得內存無法被回收,即造成了內存泄漏。下面看一個實例:
class ClassA():
def __init__(self, x=None):
self.t = x
print('object born id:%s' % str(hex(id(self))))
def f2():
c1=ClassA()
c2=ClassA()
c1.t=c2
c2.t=c1
del c1
del c2
執行f2(),會產生一個循環引用,即是del c1、c2,內存還是沒有被釋放,如果進程中存在大量的這種情況,那么進程占用的內存會不斷增大。
object born id:0x1a29f609390
object born id:0x1a29f609400
創建了c1,c2后,0x1a29f609390(c1對應的內存,記為內存1),0x1a29f609400(c2對應的內存,記為內存2)這兩塊內存的引用計數都是1,執行c1.t=c2和c2.t=c1后,這兩塊內存的引用計數變成2.
在del c1后,內存1的對象的引用計數變為1,由於不是為0,所以內存1的對象不會被銷毀,所以內存2的對象的引用數依然是2,在del c2后,同理,內存1的對象,內存2的對象的引用數都是1。刪除了c1,c2之后,這兩個對象不可能再從程序中調用,就沒有什么用處了。但是由於引用環的存在,這兩個對象的引用計數都沒有降到0,導致垃圾回收器都不會回收它們,所以就會導致內存泄露。
2.1 原理:
為了記錄下所有的容器對象, Python將每一個 容器都鏈到了一個雙向鏈表中, 之所以使用雙向鏈表是為了方便快速的在容器集合中插入和刪除對象. 有了這個 維護了所有容器對象的雙向鏈表以后, Python在垃圾回收時使用如下步驟來尋找需要釋放的對象:
- (1) 對於每一個容器對象, 設置一個gc_refs值, 並將其初始化為該對象的引用計數值
- (2) 對於每一個容器對象, 找到所有其引用的對象, 將被引用對象的gc_refs值減1
- (3) 執行完步驟2以后所有gc_refs值還大於0的對象都被非容器對象引用着, 至少存在一個非循環引用. 因此 不能釋放這些對象, 將他們放入另一個集合
- (4) 在步驟3中不能被釋放的對象, 如果他們引用着某個對象, 被引用的對象也是不能被釋放的, 因此將這些 對象也放入另一個集合中
- (5) 此時還剩下的對象都是無法到達的對象. 現在可以釋放這些對象了
2.2 優缺點:
-
優點:當然是解決了循環引用的問題。
-
缺點:標記和清除的過程效率不高。
3. 分代回收
Python同時采用了分代(generation)回收的策略。這一策略的基本假設是,存活時間越久的對象,越不可能在后面的程序中變成垃圾。我們的程序往往會產生大量的對象,許多對象很快產生和消失,但也有一些對象長期被使用。出於信任和效率,對於這樣一些“長壽”對象,我們相信它們的用處,所以減少在垃圾回收中掃描它們的頻率。
3.1 原理:
將系統中的所有內存塊根據其存活時間划分為不同的集合,每一個集合就成為一個“代”,Python默認定義了三代對象集合,垃圾收集的頻率隨着“代”的存活時間的增大而減小。也就是說,活得越長的對象,就越不可能是垃圾,就應該減少對它的垃圾收集頻率。
Python默認定義的對象分為0,1,2三代。所有的新建對象都是0代對象。當某一代對象經歷過垃圾回收,依然存活,那么它就被歸入下一代對象。垃圾回收啟動時,一定會掃描所有的0代對象。如果0代經過一定次數垃圾回收,那么就啟動對0代和1代的掃描清理。當1代也經歷了一定次數的垃圾回收后,那么會啟動對0,1,2,即對所有對象進行掃描。
4. 三種情況觸發垃圾回收:
- 1、調用gc.collect()
- 2、GC達到閥值時
- 3、程序退出時
5. 小整數對象池與intern機制
由於整數使用廣泛,為了避免為整數頻繁銷毀、申請內存空間,引入了小整數對象池。[-5,257)是提前定義好的,不會銷毀,單個字母也是。
那對於其他整數,或者其他字符串的不可變類型,如果存在重復的多個,例如:
m1 = "mark"
m2 = "mark"
m3 = "mark"
m4 = "mark"
m5 = "mark"
m6 = "mark"
print(m1 is m5)
它的運行結果是:True
如果每次聲明都開辟出一段空間,很顯然不合理,這個時候python就會使用intern機制,靠引用計數來維護。
總結:
- 1、小整數[-5,257):共用對象,常駐內存
- 2、單個字符:共用對象,常駐內存
- 3、單個單詞等不可變類型,默認開啟intern機制,共用對象,引用計數為0時銷毀。
三、調優手段
引用文章:https://blog.csdn.net/zxmzhaoxuan/article/details/82492515
1. 手動垃圾回收
對Python的垃圾回收進行調優的一個最簡單的手段便是關閉自動回收, 根據情況手動觸發. 例如在用Python開發游戲時, 可以在一局游戲的開始關閉GC, 然后在該局游戲結束后手動調用一次GC清理內存. 這樣能完全避免在游戲過程中因此 GC造成卡頓. 但是缺點是在游戲過程中可能因為內存溢出導致游戲崩潰.
2. 調高垃圾回收閾值
相比完全手動的垃圾回收, 一個更溫和的方法是調高垃圾回收的閾值. 例如一個游戲可能在某個時刻產生大量的子彈對象(假如是2000個). 而此時Python的垃圾回收的threshold0為1000. 則一次垃圾回收會被觸發, 但這2000個子彈對象並不需要被回收. 如果此時 Python的垃圾回收的threshold0為10000, 則不會觸發垃圾回收. 若干秒后, 這些子彈命中目標被刪除, 內存被引用計數機制 自動釋放, 一次(可能很耗時的)垃圾回收被完全的避免了.
調高閾值的方法能在一定程度上避免內存溢出的問題(但不能完全避免), 同時可能減少可觀的垃圾回收開銷. 根據具體項目 的不同, 甚至是程序輸入的不同, 合適的閾值也不同. 因此需要反復測試找到一個合適的閾值, 這也算調高閾值這種手段 的一個缺點.
3. 避免循環引用
一個可能更好的方法是使用良好的編程習慣盡可能的避免循環引用. 兩種常見的手段包括: 手動解循環引用和使用弱引用.
3.1 手動解循環引用
手動解循環引用指在編寫代碼時寫好解開循環引用的代碼, 在一個對象使用結束不再需要時調用. 例如:
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()
3.2 使用弱引用
弱引用指當引用一個對象時, 不增加該對象的引用計數, 當需要使用到該對象的時候需要首先檢查該對象是否還存在. 弱引用的實現方式有多種, Python自帶一個弱引用庫weakref, 其詳細文檔參加這里. 使用weakref改寫我們的代碼:
def test4():
a = A()
b = B()
a.child = weakref.ref(b)
b.parent = weakref.ref(a)
除了使用Python自帶的weakref庫以外, 通常我們也可以根據自己項目的業務邏輯實現弱引用. 例如在游戲開發中, 通常很多對象都是有 其唯一的ID的. 在引用一個對象時我們可以保存其ID而不是直接引用該對象. 在需要使用該對象的時候首先根據ID去檢查該對象是否存在.