當(dāng)一門語(yǔ)言應(yīng)用熟練以后凌简,就要向底層鉆研,搞懂其原理恃逻。如果面試大公司雏搂,比如阿里巴巴這種。他們比較喜歡問(wèn)你這些底層的東西寇损。因?yàn)閼?yīng)用這層凸郑,他們很難篩出人來(lái)。
不管何種語(yǔ)言矛市,程序運(yùn)行中都會(huì)耗費(fèi)很多資源芙沥,如果有些應(yīng)用完成以后,資源還不釋放浊吏,運(yùn)行性能就會(huì)越來(lái)越低而昨,甚至發(fā)生OOM(out of memery)問(wèn)題
無(wú)論何種垃圾收集機(jī)制, 一般都是兩階段: 垃圾檢測(cè)和垃圾回收.
在Python中, 大多數(shù)對(duì)象的生命周期都是通過(guò)對(duì)象的引用計(jì)數(shù)來(lái)管理的.
問(wèn)題: 但是存在循環(huán)引用的問(wèn)題: a 引用 b, b 引用 a, 導(dǎo)致每一個(gè)對(duì)象的引用計(jì)數(shù)都不為0, 所占用的內(nèi)存永遠(yuǎn)不會(huì)被回收
要解決循環(huán)引用: 必需引入其他垃圾收集技術(shù)來(lái)打破循環(huán)引用. Python中使用了標(biāo)記-清除以及分代收集
即, Python 中垃圾回收機(jī)制: 引用計(jì)數(shù)(主要), 標(biāo)記清除, 分代收集(輔助)
引用計(jì)數(shù), 意味著必須在每次分配和釋放內(nèi)存的時(shí)候, 加入管理引用計(jì)數(shù)的動(dòng)作
引用計(jì)數(shù)的優(yōu)點(diǎn): 最直觀最簡(jiǎn)單, 實(shí)時(shí)性, 任何內(nèi)存, 一旦沒(méi)有指向它的引用, 就會(huì)立即被回收
Python中,主要依靠gc(garbage collector)模塊的引用計(jì)數(shù)技術(shù)來(lái)進(jìn)行垃圾回收找田。所謂引用計(jì)數(shù)歌憨,就是考慮到Python中變量的本質(zhì)不是內(nèi)存中一塊存儲(chǔ)數(shù)據(jù)的區(qū)域,而是對(duì)一塊內(nèi)存數(shù)據(jù)區(qū)域的引用午阵。所以python可以給所有的對(duì)象(內(nèi)存中的區(qū)域)維護(hù)一個(gè)引用計(jì)數(shù)的屬性躺孝,在一個(gè)引用被創(chuàng)建或復(fù)制的時(shí)候享扔,讓python,把相關(guān)對(duì)象的引用計(jì)數(shù)+1底桂;相反當(dāng)引用被銷毀的時(shí)候就把相關(guān)對(duì)象的引用計(jì)數(shù)-1植袍。當(dāng)對(duì)象的引用計(jì)數(shù)減到0時(shí),自然就可以認(rèn)為整個(gè)python中不會(huì)再有變量引用這個(gè)對(duì)象籽懦,所以就可以把這個(gè)對(duì)象所占據(jù)的內(nèi)存空間釋放出來(lái)了于个。
引用計(jì)數(shù)技術(shù)在每次引用創(chuàng)建和銷毀時(shí)都要多做一些操作,這可能是一個(gè)小缺點(diǎn)暮顺,當(dāng)創(chuàng)建和銷毀很頻繁的時(shí)候難免帶來(lái)一些效率上的不足厅篓。但是其最大的好處就是實(shí)時(shí)性,其他語(yǔ)言當(dāng)中捶码,垃圾回收可能只能在一些固定的時(shí)間點(diǎn)上進(jìn)行羽氮,比如當(dāng)內(nèi)存分配失敗的時(shí)候進(jìn)行垃圾回收,而引用計(jì)數(shù)技術(shù)可以動(dòng)態(tài)地進(jìn)行內(nèi)存的管理惫恼。
一档押、變量與對(duì)象
關(guān)系圖如下:
image
1、變量祈纯,通過(guò)變量指針引用對(duì)象
變量指針指向具體對(duì)象的內(nèi)存空間令宿,取對(duì)象的值。
2腕窥、對(duì)象粒没,類型已知,每個(gè)對(duì)象都包含一個(gè)頭部信息(頭部信息:類型標(biāo)識(shí)符和引用計(jì)數(shù)器)
注意:
變量名沒(méi)有類型簇爆,類型屬于對(duì)象(因?yàn)樽兞恳脤?duì)象癞松,所以類型隨對(duì)象),變量引用什么類型的對(duì)象入蛆,變量就是什么類型的响蓉。
>>> a=123
>>> b=a
>>> id(a)
4372738752
>>> id(b)
4372738752
>>> a=456
>>> id(a)
4374261616
>>> id(b)
4372738752
PS:id()是python的內(nèi)置函數(shù),用于返回對(duì)象的身份安寺,即對(duì)象的內(nèi)存地址厕妖。
>>> var1=object
>>> var2=var1
>>> id(var1)
4372473808
>>> id(var2)
4372473808
3、引用所指判斷
通過(guò)is進(jìn)行引用所指判斷挑庶,is是用來(lái)判斷兩個(gè)引用所指的對(duì)象是否相同言秸。
整數(shù)
>>> a=1
>>> b=1
>>> print(a is b)
True
短字符串
>>> c="good"
>>> d="good"
>>> print(c is d)
True
長(zhǎng)字符串
>>> e="very good"
>>> f="very good"
>>> print(e is f)
False
列表
>>> g=[]
>>> h=[]
>>> print(g is h)
False
由運(yùn)行結(jié)果可知:
1、Python緩存了整數(shù)和短字符串迎捺,因此每個(gè)對(duì)象在內(nèi)存中只存有一份举畸,引用所指對(duì)象就是相同的,即使使用賦值語(yǔ)句凳枝,也只是創(chuàng)造新的引用抄沮,而不是對(duì)象本身跋核;
2、Python沒(méi)有緩存長(zhǎng)字符串叛买、列表及其他對(duì)象砂代,可以由多個(gè)相同的對(duì)象,可以使用賦值語(yǔ)句創(chuàng)建出新的對(duì)象率挣。
二刻伊、引用計(jì)數(shù)
在Python中,每個(gè)對(duì)象都有指向該對(duì)象的引用總數(shù)---引用計(jì)數(shù)
查看對(duì)象的引用計(jì)數(shù):sys.getrefcount()
In [2]: import sys
In [3]: a=[1,2,3]
In [4]: getrefcount(a)
Out[4]: 2
In [5]: b=a
In [6]: getrefcount(a)
Out[6]: 3
In [7]: getrefcount(b)
Out[7]: 3
Python的一個(gè)容器對(duì)象(比如:表椒功、詞典等)捶箱,可以包含多個(gè)對(duì)象。
In [12]: a=[1,2,3,4,5]
In [13]: b=a
In [14]: a is b
Out[14]: True
In [15]: a[0]=6
In [16]: a
Out[16]: [6, 2, 3, 4, 5]
In [17]: a is b
Out[17]: True
In [18]: b
Out[18]: [6, 2, 3, 4, 5]
由上可見(jiàn)动漾,實(shí)際上丁屎,容器對(duì)象中包含的并不是元素對(duì)象本身,是指向各個(gè)元素對(duì)象的引用旱眯。
3晨川、引用計(jì)數(shù)增加
1、對(duì)象被創(chuàng)建
2键思、另外的別人被創(chuàng)建
3础爬、作為容器對(duì)象的一個(gè)元素
4、被作為參數(shù)傳遞給函數(shù):foo(x)
4吼鳞、引用計(jì)數(shù)減少
1看蚜、對(duì)象的別名被顯式的銷毀
2、對(duì)象的一個(gè)別名被賦值給其他對(duì)象
3赔桌、對(duì)象從一個(gè)窗口對(duì)象中移除供炎,或,窗口對(duì)象本身被銷毀
4疾党、一個(gè)本地引用離開(kāi)了它的作用域音诫,比如上面的foo(x)函數(shù)結(jié)束時(shí),x指向的對(duì)象引用減1雪位。
引用計(jì)數(shù)法有很明顯的優(yōu)點(diǎn):
高效
運(yùn)行期沒(méi)有停頓 可以類比一下Ruby的垃圾回收機(jī)制竭钝,也就是 實(shí)時(shí)性:一旦沒(méi)有引用,內(nèi)存就直接釋放了雹洗。不用像其他機(jī)制等到特定時(shí)機(jī)香罐。實(shí)時(shí)性還帶來(lái)一個(gè)好處:處理回收內(nèi)存的時(shí)間分?jǐn)偟搅似綍r(shí)。
對(duì)象有確定的生命周期
易于實(shí)現(xiàn)
原始的引用計(jì)數(shù)法也有明顯的缺點(diǎn):
維護(hù)引用計(jì)數(shù)消耗資源时肿,維護(hù)引用計(jì)數(shù)的次數(shù)和引用賦值成正比庇茫,而不像mark and sweep等基本與回收的內(nèi)存數(shù)量有關(guān)。
無(wú)法解決循環(huán)引用的問(wèn)題螃成。A和B相互引用而再?zèng)]有外部引用A與B中的任何一個(gè)旦签,它們的引用計(jì)數(shù)都為1查坪,但顯然應(yīng)該被回收。
循環(huán)引用的示例:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
當(dāng)Python中的對(duì)象越來(lái)越多宁炫,占據(jù)越來(lái)越大的內(nèi)存偿曙,啟動(dòng)垃圾回收(garbage collection)笑陈,將沒(méi)用的對(duì)象清除。
為了解決這兩個(gè)致命弱點(diǎn)滤灯,Python又引入了以下兩種GC機(jī)制蒸健。
標(biāo)記-清除的回收機(jī)制
針對(duì)循環(huán)引用這個(gè)問(wèn)題,比如有兩個(gè)對(duì)象互相引用了對(duì)方汰瘫,當(dāng)外界沒(méi)有對(duì)他們有任何引用,也就是說(shuō)他們各自的引用計(jì)數(shù)都只有1的時(shí)候,如果可以識(shí)別出這個(gè)循環(huán)引用袍辞,把它們屬于循環(huán)的計(jì)數(shù)減掉的話,就可以看到他們的真實(shí)引用計(jì)數(shù)了常摧〗劣酰基于這樣一種考慮,有一種方法落午,比如從對(duì)象A出發(fā)谎懦,沿著引用尋找到對(duì)象B,把對(duì)象B的引用計(jì)數(shù)減去1溃斋;然后沿著B對(duì)A的引用回到A界拦,把A的引用計(jì)數(shù)減1,這樣就可以把這層循環(huán)引用關(guān)系給去掉了梗劫。
不過(guò)這么做還有一個(gè)考慮不周的地方享甸。假如A對(duì)B的引用是單向的, 在到達(dá)B之前我不知道B是否也引用了A梳侨,這樣子先給B減1的話就會(huì)使得B稱為不可達(dá)的對(duì)象了蛉威。為了解決這個(gè)問(wèn)題,python中常常把內(nèi)存塊一分為二走哺,將一部分用于保存真的引用計(jì)數(shù)蚯嫌,另一部分拿來(lái)做為一個(gè)引用計(jì)數(shù)的副本,在這個(gè)副本上做一些實(shí)驗(yàn)丙躏。比如在副本中維護(hù)兩張鏈表择示,一張里面放不可被回收的對(duì)象合集,另一張里面放被標(biāo)記為可以被回收(計(jì)數(shù)經(jīng)過(guò)上面所說(shuō)的操作減為0)的對(duì)象彼哼,然后再到后者中找一些被前者表中一些對(duì)象直接或間接單向引用的對(duì)象对妄,把這些移動(dòng)到前面的表里面。這樣就可以讓不應(yīng)該被回收的對(duì)象不會(huì)被回收敢朱,應(yīng)該被回收的對(duì)象都被回收了剪菱。
分代回收
分代回收策略著眼于提升垃圾回收的效率摩瞎。研究表明,任何語(yǔ)言孝常,任何環(huán)境的編程中旗们,對(duì)于變量在內(nèi)存中的創(chuàng)建/銷毀,總有頻繁和不那么頻繁的构灸。比如任何程序中總有生命周期是全局的上渴、部分的變量。
Python將所有的對(duì)象分為0喜颁,1稠氮,2三代;
所有的新建對(duì)象都是0代對(duì)象半开;
當(dāng)某一代對(duì)象經(jīng)歷過(guò)垃圾回收隔披,依然存活,就被歸入下一代對(duì)象寂拆。
gc模塊提供一個(gè)接口給開(kāi)發(fā)者設(shè)置垃圾回收的選項(xiàng)奢米。上面說(shuō)到,采用引用計(jì)數(shù)的方法管理內(nèi)存的一個(gè)缺陷是循環(huán)引用纠永,而gc模塊的一個(gè)主要功能就是解決循環(huán)引用的問(wèn)題鬓长。
常用函數(shù):
gc.set_debug(flags)
設(shè)置gc的debug日志,一般設(shè)置為gc.DEBUG_LEAK
gc.collect([generation])
顯式進(jìn)行垃圾回收尝江,可以輸入?yún)?shù)涉波,0代表只檢查第一代的對(duì)象,1代表檢查一茂装,二代的對(duì)象怠蹂,
2代表檢查一,二少态,三代的對(duì)象城侧,如果不傳參數(shù),執(zhí)行一個(gè)full collection彼妻,也就是等于傳2嫌佑。
返回不可達(dá)(unreachable objects)對(duì)象的數(shù)目
gc.set_threshold(threshold0[, threshold1[, threshold2])
設(shè)置自動(dòng)執(zhí)行垃圾回收的頻率。
gc.get_count()
獲取當(dāng)前自動(dòng)執(zhí)行垃圾回收的計(jì)數(shù)器侨歉,返回一個(gè)長(zhǎng)度為3的列表
gc模塊的自動(dòng)垃圾回收機(jī)制
必須要import gc模塊屋摇,并且is_enable()=True 才會(huì)啟動(dòng)自動(dòng)垃圾回收。
這個(gè)機(jī)制的主要作用就是發(fā)現(xiàn)并處理不可達(dá)的垃圾對(duì)象幽邓。
垃圾回收=垃圾檢查+垃圾回收
在Python中炮温,采用分代收集的方法。把對(duì)象分為三代牵舵,一開(kāi)始柒啤,對(duì)象在創(chuàng)建的時(shí)候倦挂,放在一代中,如果在一次一代的垃圾檢查中担巩,改對(duì)象存活下來(lái)方援,就會(huì)被放到二代中,同理在一次二代的垃圾檢查中涛癌,該對(duì)象存活下來(lái)犯戏,就會(huì)被放到三代中。
gc模塊里面會(huì)有一個(gè)長(zhǎng)度為3的列表的計(jì)數(shù)器拳话,可以通過(guò)gc.get_count()獲取先匪。
現(xiàn)在電腦的資源已經(jīng)沒(méi)有那么緊張了,很多語(yǔ)言不像C語(yǔ)言那樣假颇,要手動(dòng)釋放資源胚鸯,不然很容易出現(xiàn)野指針。Java, Python內(nèi)存都是自動(dòng)管理笨鸡,自動(dòng)回收垃圾。
如果我們知道這些原理坦冠,在寫敲代碼的時(shí)候形耗,就會(huì)避免一些陷阱,能夠提高程序性能辙浑。
如果垃圾回收不是那么好理解激涤,我們可以用現(xiàn)實(shí)生活中的例子來(lái)聯(lián)想。
又提到了開(kāi)發(fā)商來(lái)拍地判呕。
每拍一快地倦踢,房管局都要登記一下,這個(gè)開(kāi)發(fā)商捂地的數(shù)量又加一侠草。如果這塊地使用完了辱挥,開(kāi)發(fā)商捂地的數(shù)目減一。這便是引用計(jì)數(shù)边涕。
如果這塊地晤碘,一女二嫁,有糾紛功蜓。陷入循環(huán)死結(jié)了园爷。政府就各個(gè)擊破,請(qǐng)他們喝茶式撼,請(qǐng)他們都退出童社。這個(gè)就是標(biāo)記-清除。
如果這塊地的開(kāi)發(fā)商后臺(tái)特別強(qiáng)大著隆,捂著就是不開(kāi)發(fā)扰楼。地方政府就只能采取一貫的做法甘改,“讓后代去解決”。這就是分代回收灭抑。