下載Python源碼:https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgzhtml
解壓獲得文件夾:python
咱們主要關注Include中的".h"文件以及Objects目錄中的".c"文件。緩存
咱們從Include和Objects中的文件類型就能夠看出Python解釋器是C語言編寫的。app
在Include文件夾中,所有都是".h"文件。函數
這些C語言頭文件中主要存放着宏、函數聲明、結構體聲明、全局變量等。性能
咱們在Python中全部的類都繼承自Object,因此在這個C語言的object.h中,咱們能夠看看是如何實現的。優化
咱們首先看object.h文件內容(小部分):spa
#define _PyObject_HEAD_EXTRA \ struct _object *_ob_next; \ struct _object *_ob_prev; typedef struct _object { // 維護雙向鏈表refchain _PyObject_HEAD_EXTRA // 引用計數 Py_ssize_t ob_refcnt; // 數據的類型 struct _typeobject *ob_type; } PyObject; typedef struct { PyObject ob_base; // 數據類型爲多元素時,維護一個容量個數 Py_ssize_t ob_size; /* Number of items in variable part */ } PyVarObject;
咱們能夠從上面的源碼中看到,兩個結構體PyObject和PyVarObject,區別是PyVarObject多一個ob_size屬性,這個屬性表明的是元素的個數(例如list、dict中元素的個數)。3d
因此,這兩個結構體,分別對應不一樣類型的數據的頭(Python中任何數據的定義,都會有這個頭):指針
PyObject:float
PyVarObject:list、dict、tuple、set、int、str、bool
由於Python中的int是不限制長度的,因此底層實現是用的str,因此int也屬於PyVarObject陣營。Python中的bool其實是0和1,因此也是int,也屬於PyVarObject陣營。
typedef struct { PyObject_HEAD double ob_fval; } PyFloatObject;
咱們以float類型爲例,能夠看到建立一個float類型的數據,其實是建立了一個PyFloatObject結構體的實例。
PyFloatObject結構體中包含了一個PyObject_HEAD(這就是object.h中的PyObject),以及一個double ob_fval,這個double變量就是咱們存放的值。
咱們以Python中的實際操做,來看源碼中的過程:
1)python中定義變量v = 0.3:
源碼流程:
a.開闢內存(內存大小,是sizeof(PyFloatObject))
b.初始化
ob_fval=0.3
ob_type=float
ob_refcnt=1
c.將對象加入雙向鏈表refchain中
2)python執行操做name=v:
源碼流程:
ob_refcnt+=1
3)python執行操做del v:
源碼流程:
ob_refcnt-=1
4)python執行
def func(arg): print(arg) func(name)
源碼流程:
執行時開闢棧:ob_refcnt+=1
結束時銷燬棧:ob_refcnt-=1
5)python執行del name:
源碼流程:
ob_refcnt-=1
在這幾回操做中,每次進行ob_refcnt-=1的時候都會判斷ob_refcnt是否等於0。若是是0,這將其歸爲垃圾,按理說GC回收器應該將其回收,請看第二節。
在第一節中,若是float變量的引用都被刪除,引用計數爲0之後,按理說GC回收器應該對其進行回收。
但編譯器認爲,用戶常常都要定義float類型的變量,因此他將該PyFloatObject對象從refchain鏈表中拿出來,而且放到另外一個單向鏈表中,這個單向鏈表就是緩存(叫free_list)。
咱們作個驗證:
>>> v = 8.9
>>> name = v
>>> del v >>> id(name) 1706304905888 >>> del name >>> xx = 9.0 >>> id(xx) 1706304905888 >>>
能夠看到,name的id爲1706304905888,刪除name後,由建立了一個float變量xx,結果xx的id仍是爲170630490588。這就驗證了緩存的機制。
爲何要使用緩存(free_list)?
由於回收內存空間和開闢內存空間都要消耗時間,因此,若是將空間放到緩存中,有新的float變量被定義的話,直接從緩存中拿到地址,從新進行一次初始化,並將新的值賦給ob_fval便可。
注意,這裏的單向鏈表(free_list)只是針對PyFloatObject類型的。並且這個鏈表有最大長度100。能夠在floatobject.c中看到相關定義:
#ifndef PyFloat_MAXFREELIST // 定義free_list的最大長度 #define PyFloat_MAXFREELIST 100 #endif // 用numfree來表示當前free_list有多長 static int numfree = 0; // free_list指針 static PyFloatObject *free_list = NULL;
例如同時有1000個float變量的引用計數變爲0,則納入free_list的只有100個,其他900個可能會被回收。
在float中,free_list的最大長度是100,而在其餘的數據類型中,最大長度可能不同。
例如list的free_list的最大長度爲80:
#ifndef PyList_MAXFREELIST #define PyList_MAXFREELIST 80 #endif static PyListObject *free_list[PyList_MAXFREELIST]; static int numfree = 0;
dict也爲80:
#ifndef PyDict_MAXFREELIST #define PyDict_MAXFREELIST 80 #endif static PyDictObject *free_list[PyDict_MAXFREELIST]; static int numfree = 0; static PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST]; static int numfreekeys = 0;
也不是全部的數據類型都使用free_list緩存機制,例如int用的是小數據池進行優化:
#ifndef NSMALLPOSINTS #define NSMALLPOSINTS 257 #endif #ifndef NSMALLNEGINTS #define NSMALLNEGINTS 5 #endif
Python的GC主要遵循如下原則:
引用計數器爲主,標記清除和分代回收爲輔。
循環引用通常發生在列表、字典、對象等容器類對象,他們之間能夠互相嵌套,例如:
a = [1, 2] b = [4, 5] # b的引用計數會加1,變爲2 a.append(b) # a的引用計數變爲0 del a # b的引用計數變爲1,可是已經沒法訪問b,因此就造成了內存泄漏 del b
在這種狀況下, 內存發生了泄漏,就要利用標記清除來解決循環引用的問題。
針對那些容器類的對象,在Python中會將他們單獨放到一個雙向鏈表(非refchain)中,作按期掃描。
參考:http://www.javashuo.com/article/p-sgjyckxg-gu.html
#第一組循環引用# a = [1,2] b = [3,4] a.append(b) b.append(a) del a ## #第二組循環引用# c = [4,5] d = [5,6] c.append(d) d.append(c) del c del d #至此,原a和原c和原d所引用的對象的引用計數都爲1,b所引用的對象的引用計數爲2, e [7,8] del e
如今說明一下標記清除:代碼運行到上面這塊了,此時,咱們的本意是想清除掉c和d和e所引用的對象,而保留a和b所引用的對象。可是c和d所引用對象的引用計數都是非零,原來的簡單的方法只能清除掉e,c和d所引用對象目前還在內存中。
假設,此時咱們預先設定的週期時間到了,此時該標記清除大顯身手了。他的任務就是,在a,b,c,d四個可變對象中,找出真正須要清理的c和d,而保留a和b。
首先,他先劃分出兩撥,一撥叫root object(存活組),一撥叫unreachable(死亡組)。而後,他把各個對象的引用計數複製出來,對這個副本進行引用環的摘除。
環的摘除:假設兩個對象爲A、B,咱們從A出發,由於它有一個對B的引用,則將B的引用計數減1;而後順着引用達到B,由於B有一個對A的引用,一樣將A的引用減1,這樣,就完成了循環引用對象間環摘除。
摘除完畢,此時a的引用計數的副本是0,b的引用計數的副本是1,c和d的引用計數的副本都是0。那麼先把副本爲非0的放到存活組,副本爲0的打入死亡組。若是就這樣結束的話,就錯殺了a了,由於b還要用,咱們把a所引用的對象在內存中清除了b還能用嗎?顯然還得在審一遍,別把無辜的人也給殺了,因而他就在存活組裏,對每一個對象都分析一遍,因爲目前存活組只有b,那麼他只對b分析,由於b要存活,因此b裏的元素也要存活,因而在b中就發現了原a所指向的對象,因而就把他從死亡組中解救出來。至此,進過了一審和二審,最終把全部的任然在死亡組中的對象統統殺掉,而root object繼續存活。b所指向的對象引用計數任然是2,原a所指向的對象的引用計數仍然是1
掃描後存活組的對象,將放到另一個鏈表中去,一共有3個這樣的鏈表,表明3代。
分代回收就是指維護容器類對象的三個鏈表,3個鏈表對應三層。對最底層的鏈表掃描10次,纔對上層的鏈表掃描一次。
這實際上是爲了節省性能,儘可能少掃描對象。
認爲沒有問題常用的對象放入上一層,減小掃描次數。
因此,在Python的內存管理中,一共維護着4個鏈表,其中一個鏈表refchain用來管理通常的數據類型,例如float等。而另外3個鏈表組成分代,管理容器類數據類型。