第八篇:python的內存管理與深淺拷貝

 關於python的內存堆棧問題,本人沒有剖析過cpython解釋器源代碼,網上也是極少的參考文檔,這裏提供一個講解比較詳細的文檔,請移步https://www.jianshu.com/p/2f98dd213f04html

一.變量的存儲

值語義:變量的值直接保存在變量的存儲區裏,這種方式被咱們稱爲值語義,例如C語言,採用這種存儲方式,每個變量在內存中所佔的空間就要根據變量實際的大小而定,好比 c中 int 通常爲2個字節 而char爲一個字節。java

引用語義:變量存儲的對象值的內存地址,而不是對象值的自己實體,變量所需的存儲空間大小一致,由於變量只是保存了一個引用。也被稱爲對象語義和指針語義。變量的每一次初始化,都開闢了一個新的空間,將新內容的地址賦值給變量。python

python的變量存儲採用的是引用語義,引用和對象相分離,對象是內存中儲存數據的實體,引用指向對象。引用和對象的分離,是動態類型的核心。引用能夠隨時指向一個新的對象,即變量存儲的對象地址可被覆蓋。算法

二.引用與對象

引用:在 Python 中,變量也稱爲對象的引用。由於變量存儲的就是對象的地址。變量經過地址引用了「對象」。Python的一個容器對象(container),好比表、詞典等,能夠包含多個對象。實際上,容器對象中包含的並非元素對象自己,是指向各個元素對象的引用。編程

對象:Python 中,一切皆對象。每一個對象由:標識(identity)、類型(type)、value(值)組成。 標識用於惟一標識對象,一般對應於對象在計算機內存地址。使用內置函數 id(obj)可返回對象 obj 的標識。類型用於表示對象存儲的「數據」的類型。類型能夠限制對象的取值範圍以及可執行的操做。可使用 type(obj)得到對象的所屬類型。值表示對象所存儲的數據的信息。使用 print(obj)能夠直接打印出值。api

Python 是動態類型語言,變量不須要顯式聲明類型。根據變量引用的對象,Python 解釋器自動肯定數據類型。數據結構

str1 = 'abcd'  # 不可變數據類型表明                                    
list1 = [str1, 2, [3, 6]]  # 可變類型的表明                            
print("變量str1儲存的內存地址,即字符串對象'abcd'的內存地址:%s" % id(str1))
print("變量list1儲存的內存地址,即列表對象'abcd'的內存地址:%s" % id(list1))
print("list1第一個元素儲存的內存地址,即變量str1儲存的內存地址:%s" % id(list1[0]))
print("list1第三個元素儲存的內存地址,即列表對象'abcd'的內存地址:%s" % id(list1[2]))

在 python 中咱們能夠經過 id 函數來獲取某個 python 對象的內存地址,或者能夠經過調用對象的__repr__魔術函數來獲取對象的詳細信息,可是不知你們是否想過,其實這個內存地址能夠直接加載 python 對象的。不過這是很扯淡的東西,之後編寫程序最好不要這種方式加載對象。相似多此一舉的操做。就當是瞭解一下吧:app

#在_ctypes 包中,就提供 PyObj_FromPtr 這個 api 去實現咱們的需求
import _ctypes #導入模塊
str1 = 'abcd'  # 不可變數據類型表明
list1 = [str1, 2, [3, 6]]  # 可變類型的表明
def func():
    print(1)
print("變量str1儲存的內存地址,即字符串對象'abcd'的內存地址:%s" % id(str1))
print("變量list1儲存的內存地址,即列表對象'abcd'的內存地址:%s" % id(list1))
print(_ctypes.PyObj_FromPtr(id(str1)))
print(_ctypes.PyObj_FromPtr(id(list1)))
print(id(func))
_ctypes.PyObj_FromPtr(id(func))() 

三.可變數據類型與不可變數據類型

可變類型:對象在內存地址中存儲的數據可變,便可以作增、刪、改操做,對象的內存地址不變,如列表,字典。ide

不可變類型:對象在內存地址中存儲的數據不能變化,若修改數據,實際上是從新建立新對象,而後從新賦給變量新對象的內存地址,如number、字符串、元祖。函數

建立相同內容的同類對象,可變數據類型是各個變量引用不一樣的對象。不可變數據類型會引用同一個對象(如number、字符串),除不可變容器類外(如元祖)。判斷兩個變量指向的對象是否同一個對象使用運算符 is(即內存地址是否相等),判斷兩個變量a指向的對象的數據內容是否一致【不作深層判斷】使用運算符 ==。

int1 = 5
print('變量int1儲存的內存地址,即整數2的內存地址:%s'%id(int1))
int2 = 3
int3 = int2 + 2
print('變量int1儲存的內存地址,即(int2+2)運算後獲得的對象的內存地址:%s'%id(int3),)
print('int1和int3的值是否同樣:{}。int1和int3是否指向同一個對象:{}'.format(int1 == int3, int1 is int3),end='\n'*2)

str1 = 'abcd'
print("變量str1儲存的內存地址,即字符串'abcd'的內存地址:%s"%id(str1))
str2 = 'abcd'
print("變量str1儲存的內存地址,即字符串'abcd'的內存地址:%s"%id(str2))
print('str1和str2的值是否同樣{},str1和str2是否指向同一個對象:{}'.format(str1 == str2, str1 is str2))
str2 += str1
print("變量str1儲存的內存地址,即(str1+str2)運算後的字符串對象的內存地址:%s"%id(str2))
print('修改以後的str2和str1是否指向同一個對象:{}'.format(str1 is str2),end='\n'*2)

list1 = [int1,3,[str1,'a']]
print("變量list1儲存的內存地址,即列表[int1,3,[str1,'a']]的內存地址:%s"%id(list1))
list2 = [int1,3,[str1,'a']]
print("變量list2儲存的內存地址,即列表[int1,3,[str1,'a']]的內存地址:%s"%id(list2))
print("變量list1與變量list2的值是否同樣:{},list1和list2是否指向同一個對象:{}".format(list1 == list2, list1 is list2))
print("list1第二個元素和list2第二個元素的值是否同樣:{},list1第二個元素和list2第二個元素是否指向同一個對象:{}".format(list1[1] == list2[1], list1[1] is list2[1]))
print("list1第三個元素和list2第三個元素的值是否同樣:{},list1第三個元素和list2第三個元素是否指向同一個對象:{}".format(list1[2] == list2[2], list1[2] is list2[2]))
list2 += list1
print("修改以後的list2儲存的內存地址,即列表%s的內存地址:%s"%(list2,id(list2)),end='\n'*2)

# tuple1 = (2,'a',3)
tuple1 = (2,'a',[2,3])
print("變量tuple1儲存的內存地址,即元祖%s的內存地址:%s"%(tuple1,id(tuple1)))
# tuple2 = (2,'a',3)
tuple2 = (2,'a',[2,3])
print("變量tuple2儲存的內存地址,即元祖%s的內存地址:%s"%(tuple2,id(tuple2)))
print("變量tuple1與變量tuple2的值是否同樣:{},tuple1和tuple2是否指向同一個對象:{}".format(tuple1 == tuple2, tuple1 is tuple2))
print("tuple1第三個元素和tuple2第三個元素的值是否同樣:{},tuple1第三個元素和tuple2第三個元素是否指向同一個對象:{}".format(tuple1[2] == tuple2[2], tuple1[2] is tuple2[2]))
tuple2 += tuple1
print("修改後的tuple2儲存的內存地址,即元祖%s的內存地址:%s"%(tuple2,id(tuple2)))

 看完代碼仍是不明白可變與不可變數據類型的之間的儲存區別,那咱再來看看下面的圖解吧

從上圖能夠看出列表的引用關係比較複雜,還有字典,集合,元祖等容器對象的引用可能構成很複雜的拓撲結構。咱們能夠用objgraph包來繪製其引用關係。objgraph是Python的一個第三方包。安裝以前須要安裝xdot,obigraph官網

在window系統安裝python第三方包的方法:pip install 包名

import objgraph
str1 = 'ab'
list1 = ['ab',[2,'ab']]
list2 = ['ab',[2,'ab']]
objgraph.show_refs([str1,list1,list2], filename='./ref_top.png')
list2 += list1
objgraph.show_refs([str1,list1,list2], filename='./ref_topo.png')

四.淺拷貝與深拷貝

咱們知道可變類型對象的存存儲的數據是可變的,而其的引用關係又如此複雜,若一個可變對象被多數對象引用,那麼只要任意一個引用對象對它作修改,引用它的其餘引用對象的數值也會變化。因此接下來咱們學習一個重要的知識點:python的賦值拷貝、淺拷貝與深拷貝。

賦值拷貝:直接賦值,其實就是對象的引用。

#不可變類型的賦值拷貝
str1 = 'ab'
str2 = str3 = str1
print(str2 is str1)
str1 = 'abcd'
print(str3)
print(str1 is str2)
print(str1)

#可變類型的賦值拷貝
list1 = [2,[5,'a'],'b']
list2 = list1
print(list1 is list2)
list2.append('ab')
print(list1)
淺拷貝:無論多麼複雜的數據結構,淺拷貝都只會copy一層引用。不會拷貝對象的內部的可變子對象(多層)。淺拷貝是指拷貝的只是原子對象元素的引用,
換句話說,淺拷貝產生的對象自己是新的,可是它的內容不是新的,只是對原子對象的一個引用。當咱們使用下面的操做的時候,會產生淺拷貝的效果:
list1 = [2,[5,'a'],'b']
list2 = list1
print(list1 is list2)
list2.append('ab')
print(list1,end='\n'*2)

#切片
list3 = list1[0:2]
print(list3[1] is list1[1])
list3[1].append('123')
print(list1)
print(list2,end='\n'*2)

#list.copy()
list4 = list2.copy()
print(list4[1] is list2[1],end='\n'*2)

#copy.copy()
import copy
list5 = copy.copy(list2)
print(list5[1] is list2[1])

深拷貝:深拷貝就是在內存中從新開闢一塊空間,無論數據結構多麼複雜,只要遇到可能發生改變的數據類型,就從新開闢一塊內存空間把內容複製下來,直到最後一層。

import copy
#深拷貝
list1 = [2,[5,'a'],'b']
list2 = copy.deepcopy(list1)
print(list1 is list2)
print(list1[1] is list2[1])
list2[1].append('ab')
print(list1)
print(list2,end='\n'*2)

#淺拷貝
list3 = copy.copy(list1)
print(list3[1] is list1[1])

五.引用傳遞與值傳遞

可變對象爲引用傳遞,不可變對象爲值傳遞。(函數傳值)

值傳遞: 簡單來講 對於函數輸入的參數對象,函數執行中首先生成對象的一個副本,並在執行過程當中對副本進行操做。執行結束後對象不發生改變。即在堆棧中開闢了內存空間以存放由主調函數放進來的實參的值,而後把內存地址賦值給形參變量引用,從而成爲了實參的一個副本。值傳遞的特色是被調函數對形式參數的任何操做都是做爲局部變量進行,不會影響主調函數的實參變量的值。(被調函數新開闢內存空間存放的是實參的副本值)

def test(b):
    b += 2
    print(id(b))
    print(b,end='\n'*2)
    return

a = 2
print(id(a),end='\n'*2)
test(a)
print(a)
print(id(a))

引用傳遞:當傳遞列表或者字典時,若是改變引用的值,就修改了原始的對象。(被調函數新開闢內存空間存放的是實參的地址,實際上可變類型的賦值拷貝,形參變量=實參變量)

def test(str1):
    print('形參變量str1存儲的內存地址,即形參內存地址:%s'%id(str1),end='\n'*2)
    str1[1] = "changed " #此處修改就是直接修改str1的值
    return

string = ['hello world',2,3]
print(id(string))
print(string,end='\n'*2)
test(string)  # str1 = string
print(string)
print(id(string))

六.垃圾回收機制

在許多語言中都有垃圾回收機制,好比Java和Ruby,python以 引用計數垃圾回收算法 爲主要回收機制,以 標記-清除 和 分代回收 爲輔助回收機制,三種回收機制共同協做,實現了PYTHON很是高處理效率的垃圾回收機制。

引用計數

Python裏面每個東西都是對象,他們的核心是一個結構體Py_Object,全部Python對象的頭部包含了這樣一個結構PyObject,任何一個python對象都分爲兩部分: PyObject_HEAD + 對象自己數據。每一個對象都有存有指向該對象的引用總數,即引用計數。咱們可使用sys包中的getrefcount(),來查看某個對象的引用計數。須要注意的是,當使用某個引用做爲參數,傳遞給getrefcount()時,參數實際上建立了一個臨時的引用。所以,getrefcount()所獲得的結果,會比指望的多1。當一個對象被建立出來,他的引用計數就會+1,當對象被引用的時候,計數繼續增長,當引用它的對象被刪除(del)的時候,它的引用計數就會減小。直到變爲0,說明沒有任何引用指向該對象,該對象就成爲要被回收的垃圾了。當垃圾回收啓動時,Python掃描到這個引用計數爲0的對象,就將它所佔據的內存清空,Python虛擬機就會回收這個對象的內存。咱們也能夠手動啓動垃圾回收,即便用gc.collect()。

1.致使引用計數+1的狀況:

  1. 對象被建立,例如a=23
  2. 對象被引用,例如b=a
  3. 對象被做爲參數,傳入到一個函數中,例如func(a)
  4. 對象做爲一個元素,存儲在容器中,例如list1=[a,a]

2.致使引用計數-1的狀況:

  1. 對象的別名被顯式銷燬,例如del a
  2. 對象的別名被賦予新的對象,例如a=24
  3. 一個對象離開它的做用域,例如f函數執行完畢時,func函數中的局部變量(全局變量不會)
  4. 對象所在的容器被銷燬,或從容器中刪除對象

使用sys包中的getrefcount()查看引用計數:

from sys import getrefcount

int1 = 35
print('int1對象的引用總數:{}'.format(getrefcount(int1) - 1)) #對於不可類型內存中是爲一的一份,在咱們引用以前已被引用,
# 第一個輸出的結果可能與咱們調用的結果不一致,重要是看其後面被引用的變化量。
str1 = 'abc'
var = getrefcount(str1) - 1 #經過減去差值,可以使輸出結果與咱們建立對象時開始計數的引用總數一致。
print('str11對象的引用總數:{}'.format(getrefcount(str1) - var),end='\n'*2)

list1 = [5,int1,str1]
print('int1對象的引用總數:{}'.format(getrefcount(int1) - 1))
print('str1對象的引用總數:{}'.format(getrefcount(str1) - var))
print('list1對象的引用總數:{}'.format(getrefcount(list1) - 1),end='\n'*2)

list2 = list1
print('int1對象的引用總數:{}'.format(getrefcount(int1) - 1))
print('str1對象的引用總數:{}'.format(getrefcount(str1) - var))
print('list1對象的引用總數:{}'.format(getrefcount(list1) - 1))
print('list2對象的引用總數:{}'.format(getrefcount(list2) - 1),end='\n'*2)

list3 = list1.copy()
print('int1對象的引用總數:{}'.format(getrefcount(int1) - 1))
print('str1對象的引用總數:{}'.format(getrefcount(str1) - var))
print('list1對象的引用總數:{}'.format(getrefcount(list1) - 1))
print('list2對象的引用總數:{}'.format(getrefcount(list2) - 1))
print('list3對象的引用總數:{}'.format(getrefcount(list3) - 1),end='\n'*2)

 引用計數法有很明顯的優勢:

  1. 高效
  2. 運行期沒有停頓 能夠類比一下Ruby的垃圾回收機制,也就是 實時性:一旦沒有引用,內存就直接釋放了。不用像其餘機制等到特定時機。實時性還帶來一個好處:處理回收內存的時間分攤到了平時。
  3. 對象有肯定的生命週期
  4. 簡單,易於實現

引用計數法也有明顯的缺點: 

  1. 維護引用計數消耗資源,維護引用計數的次數和引用賦值成正比,而不像mark and sweep等基本與回收的內存數量有關。
  2. 沒法解決循環引用的問題。
from sys import getrefcount
list1 = [1]
print("建立列表對象[1],並被變量list1引用,列表對象[1]的引用計數爲:%s"%(getrefcount(list1)-1))
list2 = [2]
print("建立列表對象[2],並被變量list2引用,列表對象[1]的引用計數爲:%s"%(getrefcount(list2)-1))
list1.append(list2)
print("經過變量list1的引用對list1引用的對象進行添加變量list2的引用的操做,此時list2引用的對象的引用計數+1爲:%s"%(getrefcount(list2)-1))
list2.append(list1)
print("經過變量list2的引用對list2引用的對象進行添加變量list1的引用的操做,此時list1引用的對象的引用計數+1爲:%s"%(getrefcount(list1)-1))
print(list1)
print(list2)
import objgraph
objgraph.show_refs([list1,list2], filename='./ref_top.png')
del list1 #del只是刪除變量的引用,變量引用的對象的引用計數-1,並非刪除對象。
del list2

當循環引用再也不被變量引用時,任然保持引用計數大於0,引用計數回收機制就沒法回收,從而致使心裏泄露,一旦出現循環引用,咱們就得采起新的辦法了。上面說到python裏回收機制是以引用計數爲主,標記-清除和分代收集兩種機制爲輔。

標記-清除

標記清除算法做爲Python的輔助垃圾收集技術主要處理的是一些容器對象,好比list、dict、tuple,instance等,由於對於字符串、數值對象是不可能形成循環引用問題。Python使用一個雙向鏈表將這些容器對象組織起來。不過,這種簡單粗暴的標記清除算法也有明顯的缺點:清除非活動的對象前它必須順序掃描整個堆內存,哪怕只剩下小部分活動對象也要掃描全部對象。標記清除(Mark—Sweep)』算法是一種基於追蹤回收(tracing GC)技術實現的垃圾回收算法。它分爲兩個階段:第一階段是標記階段,GC會把全部的『活動對象』打上標記,第二階段是把那些沒有標記的對象『非活動對象』進行回收。那麼GC又是如何判斷哪些是活動對象哪些是非活動對象的呢?

 

對象之間經過引用(指針)連在一塊兒,構成一個有向圖,對象構成這個有向圖的節點,而引用關係構成這個有向圖的邊。從根對象(root object)出發,沿着有向邊遍歷對象,可達的(reachable)對象標記爲活動對象,不可達(unreachable)的對象就是要被清除的非活動對象。根對象就是全局變量、調用棧、寄存器。 mark-sweepg 在上圖中,咱們把小黑圈視爲全局變量,也就是把它做爲root object,從小黑圈出發,對象1可直達,那麼它將被標記,對象二、3可間接到達也會被標記,而4和5不可達,那麼一、二、3就是活動對象,4和5是非活動對象會被GC回收。

針對循環引用這個問題,好比有兩個對象互相引用了對方,當外界沒有對他們有任何引用,也就是說他們各自的引用計數都只有1的時候,若是能夠識別出這個循環引用,把它們屬於循環的計數減掉的話,就能夠看到他們的真實引用計數了。基於這樣一種考慮,有一種方法,好比從對象A出發,沿着引用尋找到對象B,把對象B的引用計數減去1;而後沿着B對A的引用回到A,把A的引用計數減1,這樣就能夠把這層循環引用關係給去掉了。

                                 

不過這麼作還有一個考慮不周的地方。假如A對B的引用是單向的, 在到達B以前我不知道B是否也引用了A,這樣子先給B減1的話就會使得B稱爲不可達的對象了。爲了解決這個問題,python中經常把內存塊一分爲二,將一部分用於保存真的引用計數,另外一部分拿來作爲一個引用計數的副本,在這個副本上作一些實驗。好比在副本中維護兩張鏈表,一張裏面放不可被回收的對象合集,另外一張裏面放被標記爲能夠被回收(計數通過上面所說的操做減爲0)的對象,而後再到可回收的對象鏈表中找一些被不可回收的對象鏈表中一些對象直接或間接單向引用的對象,把這些移動到不可回收的對象鏈表裏面。這樣就可讓不該該被回收的對象不會被回收,應該被回收的對象都被回收了。

分代回收

分代回收是創建在標記清除基礎上的一種輔助回收容器對象的GC機制。咱們知道標記-清除有一個明顯的缺點就是清除非活動的對象前它必須順序掃描整個堆內存爲了提升垃圾回收機制的執行效率,因而添加了分代回收機制。分代技術是一種典型的以空間換時間的技術,這也正是java裏的關鍵技術。這種思想簡單點說就是:對象存在時間越長,越可能不是垃圾,應該越少去收集。這樣的思想,能夠減小標記-清除機制所帶來的額外操做。分代就是將回收對象分紅數個代,每一個代就是一個鏈表(集合),代進行標記-清除的時間與代內對象存活時間成正比例關係。

分代回收一樣做爲Python的輔助垃圾收集技術處理那些容器對象。Python將內存根據對象的存活時間劃分爲不一樣的集合,每一個集合稱爲一個代,Python將內存分爲了3「代」,分別爲年輕代(第0代)、中年代(第1代)、老年代(第2代),他們對應的是3個鏈表,即一個代就是一個鏈表, 全部屬於同一」代」的內存塊都連接在同一個鏈表中 。它們的垃圾收集頻率與對象的存活時間的增大而減少。新建立的對象都會分配在年輕代,年輕代鏈表的總數達到上限時,Python垃圾收集機制就會被觸發,把那些能夠被回收的對象回收掉,而那些不會回收的對象就會被移到中年代去,依此類推,老年代中的對象是存活時間最久的對象,甚至是存活於整個系統的生命週期內。每一個代的threshold值表示該代最多容納對象的個數。默認狀況下,當0代超過700,或1,2代超過10,垃圾回收機制將觸發。

分代回收策略着眼於提高垃圾回收的效率。研究代表,任何語言,任何環境的編程中,對於變量在內存中的建立/銷,總有頻繁和不那麼頻繁的。好比任何程序中總有生命週期是全局的、部分的變量。而在垃圾回收的過程當中,其實在進行垃圾回收以前還要進行一步垃圾檢測,即檢查某個對象是否是垃圾,該不應被回收。當對象不少,垃圾檢測將耗費大量的時間而真的垃圾回收花不了多久。對於這種多對象程序,咱們能夠把一些進行垃圾回收頻率相近的對象稱爲「同一代」的對象。垃圾檢測的時候能夠對頻率較高的「代」多檢測幾回,反之,進行垃圾回收頻率較低的「代」能夠少檢測幾回。這樣就能夠提升垃圾回收的效率了。至於如何判斷一個對象屬於什麼代,python中採起的方法是經過其生存時間來判斷。若是在好幾回垃圾檢測中,該變量都是reachable的話,那就說明這個變量越不是垃圾,就要把這個變量往高的代移動,要減小對其進行垃圾檢測的頻率。

完整的收集流程:1.鏈表創建,2.肯定根節點,3.垃圾標記,4.垃圾回收。

gc模塊

 因爲Python 有了自動垃圾回收功能,就形成了很多初學者誤認爲沒必要再受內存泄漏的騷擾了。但若是仔細查看一下Python文檔對 __del__() 函數的描述,就知道這種好日子裏也是有陰雲的。根據以上的介紹,咱們知道了python對於垃圾回收,採起的是引用計數爲主,標記-清除+分代回收爲輔的回收策略。對於循環引用的狀況,通常的自動垃圾回收方式確定是無效了,這時候就須要顯式地調用一些操做來保證垃圾的回收和內存不泄露。這就要用到python內建的垃圾回收的擴展模塊gc模塊了,gc模塊提供一個接口給開發者設置垃圾回收的選項。上面說到,採用引用計數的方法管理內存的一個缺陷是循環引用,而gc模塊的一個主要功能就是解決循環引用的問題。它會實現上面「垃圾回收機制」部分中提到的一些策略好比「標記-清除」來進行垃圾回收。經過gc來查看不能回收掉的對象的詳細信息。

經常使用函數:

    1. gc.set_debug(flags)
      設置gc的debug日誌,通常設置爲gc.DEBUG_LEAK,調試信息會經過sys.stderr輸出。
    2. gc.collect([generation])
      顯式進行垃圾回收,能夠輸入參數,0表明只檢查第一代的對象,1表明檢查一,二代的對象,2表明檢查一,二,三代的對象,若是不傳參數,執行一個full collection,也就是等於傳2。
      返回不可達(unreachable objects)對象的數目
    3. gc.set_threshold(threshold0[, threshold1[, threshold2])
      設置自動執行垃圾回收的頻率。
    4. gc.get_count()
      獲取當前自動執行垃圾回收的計數器,返回一個長度爲3的列表
    5. gc.disable()      關閉自動的垃圾回收,改成手動
    6. gc.garbage()    
      返回一個不可到達(unreachable)並且不能回收(uncollectable)的對象列表。從Python3.4開始,該列表大多數應該是空的。
    7. gc.get_referrers(*objs)  返回直接引用任何objs的對象的列表
    8. gc.get_debug() 返回當前設置的調試設置;
    9. gc.get_referrers(*objs) 返回直接引用任何objs的對象的列表
    10. gc.get_referents(*objs) 返回任何參數直接引用的對象列表。
    11. gc.get_stats() 返回一個包含三代回收信息的列表,每代的回收信息
    12. gc.DEBUG_STATS 打印回收期間的統計信息
    13. gc.DEBUG_COLLECTABLE 打印找到可收集對象的信息
    14. gc.DEBUG_UNCOLLECTABLE 打印找到的不可收集對象的信息(不能被收集器釋放但不能被收集器釋放的對象)。這些對象將被添加到垃圾清單
    15. gc.DEBUG_SAVEALL 設置時,找到的全部不可達對象將被追加到垃圾中,而不是被釋放。這對調試泄漏程序頗有用。
    16. gc.DEBUG_LEAK 收集器須要打印有關泄漏程序的相關信息,等同於(DEBUG_COLLECTABLE | DEBUG_UNCOLLECTABLE | DEBUG_SAVEALL
import sys
import gc

a = [1] b = [2] a.append(b) b.append(a) ####此時a和b之間存在循環引用#### print(sys.getrefcount(a)-1) #結果應該是2 print(sys.getrefcount(b)-1) #結果應該是2 print(gc.isenabled(),end='\n'*2) #python默認是自動回收 gc.disable() #關閉自動回收,改成手動, print(gc.isenabled()) gc.enable() print(gc.isenabled(),end='\n'*2) del a del b print(gc.garbage) print(gc.collect()) ####gc.collect()專門用來處理這些循環引用,返回處理這些循環引用一共釋放掉的對象個數。這裏返回是2#### print(gc.garbage)
import objgraph
import gc

class Foo(object):

    def __init__(self):
        self.bar = None
        print("foo init")

    def __del__(self):
        print("foo del")


class Bar(object):

    def __init__(self):
        self.foo = None
        print("bar init")

    def __del__(self):
        print("bar del")

# gc.set_debug(gc.DEBUG_SAVEALL)
foo = Foo()
bar = Bar()
foo.bar = bar
bar.foo = foo
del foo
del bar
print(objgraph.count('Foo'))
print(objgraph.count('Bar'))
print(gc.collect())
print(objgraph.count('Foo'))
print(objgraph.count('Bar'))

這裏須要明確一下,以前對於「垃圾」二字的定義並非很明確,在這裏的這個語境下,垃圾是指在通過collect的垃圾回收以後仍然保持unreachable狀態,即沒法被回收,且沒法被用戶調用的對象應該叫作垃圾。gc模塊中有garbage這個屬性,其爲一個列表,每一項都是當前解釋器中存在的垃圾對象。通常狀況下,這個屬性始終保持爲空集。

collect返回4的緣由是由於,在A和B類對象中還默認有一個__dict__屬性,裏面有全部屬性的信息。好比對於a,有a.__dict__ = {'_b':<__main__.B instance at xxxxxxxx>}。a的__dict__和b的__dict__也是循環引用的。

有時候garbage裏也會出現那兩個__dict__,這主要是由於在前面可能設置了gc模塊的debug模式,好比gc.set_debug(gc.DEBUG_LEAK),會把全部已經回收掉的unreachable的對象也都加入到garbage裏面。set_debug還有不少參數諸如gc.DEBUG_STAT|DEBUG_COLLECTABLE|DEBUG_UNCOLLECTABLE|DEBUG_SAVEALL等等,設置了相關參數後gc模塊會自動檢測垃圾回收情況並給出實時地信息反映。

有三種狀況會觸發垃圾回收:
1.調用gc.collect(),
2.當gc模塊的計數器達到閥值的時候。
3.程序退出的時候

垃圾回收=垃圾檢查+垃圾回收

 gc模塊裏面會有一個長度爲3的列表的計數器,能夠經過gc.get_count()獲取。

例如(488,3,0),其中488是指距離上一次0代垃圾檢查,Python分配內存的數目減去釋放內存的數目,注意是內存分配,而不是引用計數的增長3是指距離上一次1代垃圾檢查,0代垃圾檢查的次數,同理,0是指距離上一次2代垃圾檢查,1代垃圾檢查的次數。

gc模快有一個自動垃圾回收的閥值,即經過gc.get_threshold函數獲取到的長度爲3的元組,這個方法返回的是(700,10,10),這也是gc的默認值。這個值的意思是說,在第0代對象數量達到700個以前,不把未被回收的對象放入第一代;而在第一代對象數量達到10個以前也不把未被回收的對象移到第二代。使用gc.set_threshold(threashold0,threshold1,threshold2)能夠手動設置這組閾值。
例如,假設閥值是(700,10,10)

    • 當計數器從(699,3,0)增長到(700,3,0),gc模塊就會執行gc.collect(0),即檢查0代對象的垃圾,並重置計數器爲(0,4,0)
    • 當計數器從(699,9,0)增長到(700,9,0),gc模塊就會執行gc.collect(1),即檢查0、1代對象的垃圾,並重置計數器爲(0,0,1)
    • 當計數器從(699,9,9)增長到(700,9,9),gc模塊就會執行gc.collect(2),即檢查0、一、2代對象的垃圾,並重置計數器爲(0,0,0)

調優手段:

  1. 項目中避免循環引用
  2. 引入gc模塊,啓動gc模塊的自動清理循環引用的對象機制

python的內存管理機制

Python中的內存管理機制的層次結構提供了4層,其中最底層則是C運行的mallocfree接口,往上的三層纔是由Python實現而且維護的。

Python在運行期間會大量地執行malloc和free的操做,頻繁地在用戶態和核心態之間進行切換,這將嚴重影響Python的執行效率。爲了加速Python的執行效 率,Python引入了一個內存池機制,用於管理對小塊內存的申請和釋放。可是它將釋放的內存放到內存池而不是返回給操做系統。

參考鏈接:https://docs.python.org/zh-cn/3.6/c-api/memory.html

     https://blog.csdn.net/zhzhl202/article/details/7547445

     https://blog.csdn.net/qq_33339479/article/details/81609159

     https://www.cnblogs.com/qq_841161825/articles/10174739.html

相關文章
相關標籤/搜索