Python 內(nèi)存管理和垃圾回收機(jī)制

內(nèi)存管理機(jī)制

可變對(duì)象/不可變對(duì)象
  • 可變對(duì)象:如列表、字典等,本質(zhì)是不論怎么改變值,他的地址都不會(huì)發(fā)生改變。
  • 不可變對(duì)象:如int、float、bool、str、元組等,當(dāng)你重新賦值時(shí),在底層他將會(huì)修改指向的數(shù)據(jù),而不會(huì)對(duì)原來(lái)指向的值進(jìn)行修改,對(duì)原來(lái)的值修改的僅時(shí)引用次數(shù)減一
內(nèi)存分配原則

python中一切皆對(duì)象,在底層中每個(gè)對(duì)象都是基于結(jié)構(gòu)體實(shí)現(xiàn)的,而這些結(jié)構(gòu)體里都有著上下指向、引用計(jì)數(shù)和數(shù)據(jù)類(lèi)型的屬性,在初始化時(shí)都會(huì)給數(shù)據(jù)的引用計(jì)數(shù)設(shè)置為1,每當(dāng)多一個(gè)指向時(shí),其引用計(jì)數(shù)就加一,每刪除一個(gè)他的指向,就引用計(jì)數(shù)減一,當(dāng)為0時(shí)就會(huì)對(duì)該數(shù)據(jù)進(jìn)行垃圾回收

緩存機(jī)制

在python的內(nèi)存管理當(dāng)中可能存在一些緩存機(jī)制(如:int、float、str、list等都有),即:將某個(gè)數(shù)據(jù)刪除時(shí),其可能不會(huì)將這個(gè)對(duì)象完全銷(xiāo)毀,而是將對(duì)象存放到一個(gè)鏈表中,當(dāng)又創(chuàng)建同類(lèi)型的對(duì)象時(shí),將會(huì)直接賦值給緩存的同類(lèi)型對(duì)象,再通過(guò)變量引用指向他,舉例:

>>> a = "xxx"
>>> id(a)
2436976108520
>>> del a
# 刪除了字符串a(chǎn)
>>> b = "yyy"
# 創(chuàng)建字符串b
>>> id(b)
# 可以發(fā)現(xiàn)內(nèi)存地址時(shí)一樣的
2436976108520

垃圾回收機(jī)制

對(duì)象引用次數(shù)

一般情況下垃圾回收基于對(duì)象引用次數(shù),當(dāng)初始化時(shí)次數(shù)為1,被其他對(duì)象引用時(shí)加1,使用del本質(zhì)則是將引用次數(shù)減一,而當(dāng)引用次數(shù)變成0以后則會(huì)自動(dòng)觸發(fā)垃圾回收機(jī)制將其回收,代碼層面上則是會(huì)調(diào)用該對(duì)象的__del__方法,舉例:

class D(dict):
    def __init__(self, name):
        self.name = name
        print("對(duì)象:{}被創(chuàng)建!".format(self.name))
    def __del__(self):
        print("對(duì)象:{}被銷(xiāo)毀!".format(self.name))
a = D('a')
a = D('b')
print("程序結(jié)束!")

結(jié)果:
對(duì)象:a被創(chuàng)建!
對(duì)象:b被創(chuàng)建!
對(duì)象:a被銷(xiāo)毀!
程序結(jié)束!
對(duì)象:b被銷(xiāo)毀!

可以看出第一句實(shí)例化A時(shí)對(duì)象被創(chuàng)建,對(duì)象的引用計(jì)數(shù)初始化為1,當(dāng)?shù)诙鋱?zhí)行時(shí),新的A對(duì)象被創(chuàng)建,新的對(duì)象引用計(jì)數(shù)為1,而舊的A對(duì)象因?yàn)閍指向了其他數(shù)據(jù),所以引用次數(shù)減一,此時(shí)舊的A對(duì)象引用次數(shù)變成0,觸發(fā)銷(xiāo)毀機(jī)制,從而自動(dòng)調(diào)用了__del__方法。當(dāng)程序結(jié)束時(shí),因?yàn)橐厥諆?nèi)存,因此新的對(duì)象A也自動(dòng)調(diào)用__del__方法。

查看引用次數(shù)

可以用sys.getrefcount()方法來(lái)查看引用次數(shù),要注意因?yàn)閷?nèi)容傳入該方法時(shí)引用也會(huì)加1,所以我們實(shí)際想知道的引用次數(shù)應(yīng)該是輸出的結(jié)果減一,舉例:

>>> a = 1000
>>> sys.getrefcount(a)
# 加上傳入方法的a,引用次數(shù)為2
2
>>> b = a
>>> sys.getrefcount(a)
# 因?yàn)閎也引用了a,所以引用次數(shù)加1
3
>>> del b
>>> sys.getrefcount(a)
# 刪除了b以后引用次數(shù)減1
2
標(biāo)記清除

前面的引用計(jì)數(shù)能夠解決一般情況下的內(nèi)存回收問(wèn)題,但是對(duì)于循環(huán)引用的情況,可能就會(huì)無(wú)法回收,從而造成內(nèi)存泄漏的問(wèn)題,例如下面代碼:

class D(dict):
    def __init__(self, name):
        self.name = name
        print("對(duì)象:{}被創(chuàng)建!".format(self.name))
    def __del__(self):
        print("對(duì)象:{}被銷(xiāo)毀!".format(self.name))

a = D('a')
b = D('b')

a['x'] = b
b['x'] = a

a = 1
b = 1
print("程序結(jié)束!")

結(jié)果:
對(duì)象:a被創(chuàng)建!
對(duì)象:b被創(chuàng)建!
程序結(jié)束!
對(duì)象:a被銷(xiāo)毀!
對(duì)象:b被銷(xiāo)毀!

可以看到上面的兩個(gè)字典類(lèi)因?yàn)榛ハ嘀赶蛄?,所以即使銷(xiāo)毀了,引用計(jì)數(shù)也永遠(yuǎn)大于0,此時(shí)垃圾回收機(jī)制也就不起作用了,所以之后即使a和b都指向了其他值,但因?yàn)樗麄冊(cè)戎赶虻淖值漕?lèi)互相有指向,引用計(jì)數(shù)不為0,導(dǎo)致他們直到程序結(jié)束內(nèi)存才被回收。此時(shí)如果想要回收,那么就需要先收集垃圾,然后再進(jìn)行回收,Python提供了gc.collect()方法用于手動(dòng)回收數(shù)據(jù),舉例:

import gc

class L(list):
    def __del__(self):
        print(self, "end")

a = L([1,2,3])
b = L([1,2,a])
a[-1] = b
del a, b
# 手動(dòng)回收第0代(后面會(huì)介紹分代回收,總共有3代)
print("gc generation 0 nums:", gc.collect(0))
print("end")

# [1, 2, [1, 2, [...]]] end
# [1, 2, [1, 2, [...]]] end
# gc generation 0 nums: 2
# end

可以看到兩個(gè)互相引用的對(duì)象被回收了,而這種手動(dòng)回收的方式就基于了標(biāo)記清除來(lái)實(shí)現(xiàn):

  1. 首先GC會(huì)對(duì)所有活動(dòng)的對(duì)象打上標(biāo)記,即一個(gè)個(gè)點(diǎn),然后他們之間的引用通過(guò)指向來(lái)表明,此時(shí)就構(gòu)成了一個(gè)有向圖
  2. 然后GC會(huì)從根對(duì)象出發(fā),沿著有向邊遍歷整個(gè)圖,而對(duì)于不可達(dá)的對(duì)象,那么就被視為需要清理的垃圾對(duì)象。
分代回收

建立在垃圾清除的基礎(chǔ)上,其將對(duì)象的活動(dòng)時(shí)間分為3代,新生的對(duì)象在0代,如果他們?cè)诘?代中能夠存活下來(lái),就會(huì)被放入1代里,當(dāng)在1代中也存活了下來(lái),再被放到2代,默認(rèn)當(dāng)對(duì)象數(shù)量減去釋放的對(duì)象數(shù)量(即當(dāng)前可達(dá)的對(duì)象數(shù)量)超過(guò)700時(shí)將會(huì)對(duì)0代對(duì)象進(jìn)行回收處理,當(dāng)進(jìn)行了10次0代回收則會(huì)觸發(fā)1代回收,當(dāng)進(jìn)行了10次1代回收則會(huì)觸發(fā)2代回收,這些配置可以通過(guò)gc.get_threshold()方法獲取,并通過(guò)gc.set_threshold()自定義,舉例:

>>> gc.get_threshold()
(700, 10, 10)
>>> gc.set_threshold(500, 5, 3)
>>> gc.get_threshold()
(500, 5, 3)

這里我們?cè)賮?lái)對(duì)前面循環(huán)引用的情況通過(guò)分代回收來(lái)查看效果,首先由于默認(rèn)的設(shè)置里是需要對(duì)象數(shù)量減去釋放數(shù)量超過(guò)700時(shí)才會(huì)觸發(fā),而這里我們使用的對(duì)象示例較少,所以需要我們調(diào)整這個(gè)觸發(fā)的閾值,然后為了更加明顯地看出回收的步驟,這里也重寫(xiě)了__new__方法,代碼如下:

import gc
gc.set_threshold(2, 10, 10)
# 第一個(gè)參數(shù)代表,如果設(shè)置為0代表禁用,這里設(shè)置2,代表第0代超過(guò)2個(gè)對(duì)象時(shí)觸發(fā)垃圾回收
# 后面兩個(gè)是對(duì)第一代和第二代的進(jìn)行回收,這里只要大于1就行了
# 等于1的話(huà)那么會(huì)不停觸發(fā)對(duì)1/2代的回收,從而導(dǎo)致對(duì)第0代的回收失敗
class D(dict):
    def __new__(self, name):
        print("對(duì)象:{}被分配!".format(name))
        return dict.__new__(self)
    def __init__(self, name):
        self.name = name
        print("對(duì)象:{}被創(chuàng)建!".format(self.name))
    def __del__(self):
        print("對(duì)象:{}被銷(xiāo)毀!".format(self.name))

print("初始時(shí)的垃圾回收計(jì)數(shù)器:", gc.get_count())
a = D('a')
b = D('b')
print("創(chuàng)建了兩個(gè)對(duì)象時(shí)的回收計(jì)數(shù)器:", gc.get_count())
a['x'] = b
b['x'] = a

a = 1
b = 2
print("修改了兩個(gè)對(duì)象時(shí)的垃圾回收計(jì)數(shù)器:", gc.get_count())
c = D('c')
# 分配空間給C時(shí),可以看到觸發(fā)了第0代的回收
print("新分配空間給對(duì)象C時(shí)的垃圾回收計(jì)數(shù)器:", gc.get_count())
print("程序結(jié)束!")

結(jié)果:
初始時(shí)的垃圾回收計(jì)數(shù)器: (0, 8, 1)
對(duì)象:a被分配!
對(duì)象:a被創(chuàng)建!
對(duì)象:b被分配!
對(duì)象:b被創(chuàng)建!
創(chuàng)建了兩個(gè)對(duì)象時(shí)的回收計(jì)數(shù)器: (2, 8, 1)
修改了兩個(gè)對(duì)象時(shí)的垃圾回收計(jì)數(shù)器: (2, 8, 1)
對(duì)象:c被分配!
對(duì)象:a被銷(xiāo)毀!
對(duì)象:b被銷(xiāo)毀!
對(duì)象:c被創(chuàng)建!
新分配空間給對(duì)象C時(shí)的垃圾回收計(jì)數(shù)器: (0, 9, 1)
程序結(jié)束!
對(duì)象:c被銷(xiāo)毀!

可以看出在我們的主要代碼跑起前已經(jīng)進(jìn)行過(guò)8次1代和1次2代的垃圾回收了,當(dāng)創(chuàng)建了兩個(gè)對(duì)象以后,0代增加了2個(gè),修改了這兩個(gè)對(duì)象的指向后,計(jì)數(shù)器看起來(lái)還是2個(gè)a和b,但是實(shí)際上因?yàn)樵瓉?lái)的兩個(gè)字典循環(huán)引用導(dǎo)致未被釋放,所以實(shí)際有4個(gè),只是有2個(gè)是不可達(dá)的,因此在給對(duì)象c分配空間時(shí)計(jì)數(shù)器增加1變成3,因?yàn)槌^(guò)了2,需要進(jìn)行一次對(duì)0代的垃圾回收,因此a和b這兩個(gè)不可達(dá)的就被銷(xiāo)毀,然后再創(chuàng)建對(duì)象c,最終程序結(jié)束,將未被釋放的對(duì)象a、b和c都銷(xiāo)毀

更多參考:
https://blog.csdn.net/it_yuan/article/details/52850270
https://www.jb51.net/article/79306.htm
http://www.itdecent.cn/p/0c37059ce224
https://testerhome.com/topics/16556

弱引用

當(dāng)引用某個(gè)數(shù)據(jù)時(shí),引用計(jì)數(shù)不會(huì)加一,假如有些數(shù)據(jù)被刪除后,希望直接被垃圾回收,就可以利用弱引用來(lái)實(shí)現(xiàn),舉例:

import weakref

s = {1,2,3}
w = weakref.ref(s)
print(w())
s.remove(1)
print(w())
del s
print(w())

# {1, 2, 3}
# {2, 3}
# None
弱引用集合
  • 示例1:
import weakref

class A: pass
class B: pass

s = {1,2,3}
w = weakref.WeakSet()
a = A()
b = B()
w.add(a)
w.add(b)
print(w.data)
del a
print(w.data)

# {<weakref at 0x000002105CAE38B8; to 'A' at 0x000002105C83A2B0>, <weakref at 0x000002105CAE3778; to 'B' at 0x000002105C9494E0>}
# {<weakref at 0x000002105CAE3778; to 'B' at 0x000002105C9494E0>}
  • 示例2:
import weakref

class A:
    def __del__(self):
        print("對(duì)象A被刪除!")

a = A()
# b是a的引用
b = a
# c是a的弱引用
c = weakref.ref(a)
# 創(chuàng)建一個(gè)弱引用集合
s = weakref.WeakSet()
# 往集合當(dāng)中添加一個(gè)對(duì)a的弱引用
s.add(a)
print("a的弱引用:", weakref.getweakrefs(a), "數(shù)量:", weakref.getweakrefcount(a))
del a
print("c指向的對(duì)象:", c())
del b
print("c指向的對(duì)象:", c())

# a的弱引用: [<weakref at 0x000001F8C66FB548; to 'A' at 0x000001F8C66FA1D0>, <weakref at 0x000001F8C6994778; to 'A' at 0x000001F8C66FA1D0>] 數(shù)量: 2
# c指向的對(duì)象: <__main__.A object at 0x000001F8C66FA1D0>
# 對(duì)象A被刪除!
# c指向的對(duì)象: None

參考:http://www.itdecent.cn/p/b94b054b8a5d

弱引用字典
import weakref

class A: pass
a = A()
b = A()
w = weakref.WeakValueDictionary()
# w = {}
# 將w改成字典,則會(huì)發(fā)現(xiàn)a沒(méi)有被回收
w["a"] = a
w["b"] = b
print(list(w.keys()))
del a
print(list(w.keys()))

# ['a', 'b']
# ['b']

可以看到將a刪除以后,弱引用字典里的a也被刪除,從而起到一個(gè)類(lèi)似緩沖的作用

參考:https://blog.csdn.net/MZP_man/article/details/99236003

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在中國(guó),一個(gè)地方從荒涼走向繁華要多久? 2013年5月。在公司附近找房子,中介推薦去一處新開(kāi)發(fā)的樓盤(pán)。那一片政府準(zhǔn)...
    滿(mǎn)九閱讀 186評(píng)論 0 0
  • 此文主要以證書(shū)生成配置為主,實(shí)現(xiàn)簡(jiǎn)單推送,部分截圖與內(nèi)容來(lái)自于互聯(lián)網(wǎng),若對(duì)大家有所幫助,還請(qǐng)給個(gè)贊O(∩_∩)O~...
    damonzero1991閱讀 443評(píng)論 0 2
  • 百嶺自回合,天開(kāi)寶樹(shù)林。 古幢靈影曳,風(fēng)竹澗泉吟。 白石參龍象,青山習(xí)道心。 網(wǎng)羅空綣戀,吾意在高深。
    江南莫之閱讀 884評(píng)論 0 12
  • ionic.css文件中替換以下css
    一只飛閱讀 636評(píng)論 0 0

友情鏈接更多精彩內(nèi)容