內(nèi)存管理機(jī)制
Python的內(nèi)存管理內(nèi)存總共分為4層(Layer0-3):
第一層Layer1的僅僅是對malloc的簡單包裝,raw memory踩娘,目的是為了兼容各個(gè)操作系統(tǒng),因?yàn)椴煌牟僮飨到y(tǒng)調(diào)用malloc的時(shí)候可能會有不同的行為結(jié)果喉祭;第二層Layer2是內(nèi)存管理機(jī)制的核心养渴,其中g(shù)c就是在這一層發(fā)揮至關(guān)重要的作用。第三層泛烙,是對象緩沖池理卑,如python對一些對象的直接操作,包括int蔽氨,list等藐唠。
對于可能被經(jīng)常使用、而且是immutable的對象鹉究,如bool類型宇立,元祖類型,小的整數(shù)自赔、長度較短的字符串等妈嘹,python會緩存在layer3,直接供python調(diào)用匿级,避免頻繁創(chuàng)建和銷毀蟋滴。
>>> a,b=1234567890123,1234567890123
>>> a is b
True
>>> a,b=(1,2,3,'a'),(1,2,3,'a')
>>> a is b
False
>>> a,b=('a'),('a')
>>> a is b
True
當(dāng)一個(gè)對象邏輯上不被使用了染厅,但并沒有被釋放痘绎,那么就存在內(nèi)存泄露,很有可能會造成程序效率低下甚至崩潰肖粮;
Python分配內(nèi)存的時(shí)候又分為大內(nèi)存和小內(nèi)存孤页。大小以256字節(jié)為界限,對于大內(nèi)存使用Malloc進(jìn)行分配涩馆,而對于小內(nèi)存則使用內(nèi)存池進(jìn)行分配行施。由于小內(nèi)存的分配和釋放是頻繁的允坚,因此內(nèi)存池的使用大大提高了python的執(zhí)行效率。
引用計(jì)數(shù)
在python中大多數(shù)對象的生命周期都是通過引用計(jì)數(shù)來管理的蛾号,引用計(jì)數(shù)也是一種最直觀最簡單的垃圾收集技術(shù)
每個(gè)python對象都有一個(gè)引用計(jì)數(shù)器稠项,用于記錄多少變量指向這個(gè)對象,可以通過sys模塊的getrefcount查詢獲得鲜结。
>>> sys.getrefcount({'a':1})
1
>>> sys.getrefcount(1)
590
每一個(gè)對象都會維護(hù)一個(gè)引用計(jì)數(shù)器展运,當(dāng)一個(gè)對象被引用的時(shí)候,它的計(jì)數(shù)器就+1精刷,當(dāng)一個(gè)對象的引用被銷毀時(shí)拗胜,計(jì)數(shù)器-1,當(dāng)這個(gè)對象的引用計(jì)數(shù)為0的時(shí)候怒允,說明這個(gè)對象已經(jīng)沒有使用了埂软,可以被釋放,就會被回收纫事,具有實(shí)時(shí)性勘畔。由于引用計(jì)數(shù)需要維護(hù)計(jì)數(shù)器等額外的操作,為了與引用計(jì)數(shù)搭配儿礼,在內(nèi)存的分配和釋放上獲得最高的效率咖杂,python因此設(shè)計(jì)了大量的內(nèi)存池機(jī)制。
下面這些情況引用計(jì)數(shù)+1:
- 對象被創(chuàng)建:a=4
- 引用被復(fù)制:y=x
- 被作為參數(shù)傳遞給函數(shù):f(x)
- 作為容器對象的一個(gè)元素:a=[1,x]
下面這些情況引用計(jì)數(shù)-1
- 離開作用域蚊夫。比如f(x)函數(shù)結(jié)束時(shí)诉字,x指向的對象引用減1。
- 引用被顯式的銷毀:del x
- 對象的一個(gè)別名被賦值給其他對象:y=1
- 對象從一個(gè)容器對象中移除:l.remove(x)
- 容器對象本身被銷毀:del l知纷。
python 的內(nèi)存管理主要以引用計(jì)數(shù)為主壤圃,引用計(jì)數(shù)機(jī)制能釋放大部分無用對象,除了一種情況琅轧,循環(huán)引用伍绳,因?yàn)檠h(huán)引用的對象引用計(jì)數(shù)器永不為0.
循環(huán)引用,就是一個(gè)對象直接或者間接引用自己本身,導(dǎo)致計(jì)數(shù)器不為0:
class Test(object):
pass
t1 = Test()
t1.a = t1
a, b = Test(), Test()
a.attr_b = b
b.attr_a = a
l1=[]
l2=[]
l1.append(l2)
l2.append(l1)
標(biāo)記清除
標(biāo)記清除算法作為Python的輔助垃圾收集技術(shù)主要處理的是一些容器對象乍桂,比如list冲杀、dict、tuple睹酌,instance等权谁,因?yàn)閷τ谧址?shù)值對象是不可能造成循環(huán)引用問題憋沿。標(biāo)記清除和分代回收就是為了解決循環(huán)引用而生的旺芽。
標(biāo)記清除會使用垃圾收集監(jiān)控對象,講對象放到鏈表上,被垃圾收集監(jiān)控的對象并非只有垃圾收集機(jī)制才能回收采章,正常的引用計(jì)數(shù)就能銷毀一個(gè)被納入垃圾收集機(jī)制監(jiān)控的對象运嗜。雖然有很多對象掛在垃圾收集機(jī)制監(jiān)控的鏈表上,但實(shí)際更多時(shí)候悯舟,是引用計(jì)數(shù)機(jī)制在維護(hù)這些對象担租,只有對引用計(jì)數(shù)無能為力的循環(huán)引用,垃圾收集機(jī)制才起作用抵怎,事實(shí)上翩活,除循環(huán)引用外的對象,垃圾收集機(jī)制是無能為力的便贵,因?yàn)閽煸诶占瘷C(jī)制上的對象都是引用計(jì)數(shù)不為0的菠镇,如果是0,早就被引用計(jì)數(shù)清理了承璃。
del x 并不一定會調(diào)用__del__
方法利耍,只有引用計(jì)數(shù) == 0時(shí),__del__
()才會被執(zhí)行盔粹,如果一個(gè)Python對象定義了__del__
這個(gè)方法, Python的垃圾回收機(jī)制即使發(fā)現(xiàn)該對象不可到達(dá) 也不會釋放他. 原因是__del__
這個(gè)方式是當(dāng)一個(gè)Python對象引用計(jì)數(shù)為0即將被刪除前調(diào)用用來做清理工作的.由于垃圾回收找到的需要釋放的對象中往往存在循環(huán)引用的情況, 對于循環(huán)引用的對象a和b, 應(yīng)該先調(diào)用哪 一個(gè)對象的__del__
是無法決定的,當(dāng)執(zhí)行垃圾回收的時(shí)候隘梨,會將循環(huán)引用中定義了__del__
函數(shù)的類實(shí)例放到gc.garbage列表, 因此Python垃圾回收機(jī)制就放棄釋放這些對象,會造成事實(shí)上的內(nèi)存泄露, 轉(zhuǎn)而將這些對象保存起來, 應(yīng)避免在代碼中定義__del__
方法.
import time
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__方法被調(diào)用")
# 創(chuàng)建對象
cat = Test("貓")
cat2 = cat
del cat
del cat2
time.sleep(10)
垃圾回收時(shí)舷嗡,Python不能進(jìn)行其它的任務(wù)轴猎,會造成程序卡頓。頻繁的垃圾回收將大大降低Python的工作效率进萄。當(dāng)Python運(yùn)行時(shí)捻脖,會記錄其中分配對象和取消分配對象的次數(shù)。當(dāng)兩者的差值高于某個(gè)閾值時(shí)中鼠,垃圾回收才會啟動(dòng)可婶。
>>> import gc
>>> print(gc.get_threshold())
(700, 10, 10)
>>>
700是垃圾回收啟動(dòng)的閾值,后面兩個(gè)10和分代回收有關(guān)援雇,也就是新增對象與釋放對象的差值為700時(shí)矛渴,進(jìn)行一次垃圾回收,主要目標(biāo)是循環(huán)引用惫搏,這個(gè)時(shí)候會造成卡頓
gc.enable(); gc.disable(); gc.isenabled() #開啟gc(默認(rèn))具温;關(guān)閉gc;判斷gc是否開啟
gc.collection() #執(zhí)行一次垃圾回收筐赔,不管gc是否處于開啟狀態(tài)都能使用
gc.set_threshold(t0, t1, t2) #設(shè)置垃圾回收閾值铣猩;
gc.get_threshold() # 獲得當(dāng)前的垃圾回收閾值
gc.get_objects() #獲取所有被垃圾回收器監(jiān)控管理的對象
gc.get_referents(obj) #返回obj對象直接指向的對象
gc.get_referrers(obj) #返回所有直接指向obj的對象
分代回收
同時(shí),分代回收是建立在標(biāo)記清除技術(shù)基礎(chǔ)之上川陆。分代回收同樣作為Python的輔助垃圾收集技術(shù)處理那些容器對象
Python將所有的對象分為0剂习,1,2三代较沪。所有的新建對象都是0代對象鳞绕。當(dāng)某一代對象經(jīng)歷過垃圾回收,依然存活尸曼,那么它就被歸入下一代對象们何。get_threshold()返回的(700, 10, 10)返回的兩個(gè)10。也就是說控轿,每10次0代垃圾回收冤竹,會配合1次1代的垃圾回收;而每10次1代的垃圾回收茬射,才會有1次的2代垃圾回收鹦蠕。理論上,存活時(shí)間久的對象在抛,使用的越多钟病,越不容易被回收,這也是分代回收設(shè)計(jì)的思想刚梭。
內(nèi)存泄漏
本文是在python2環(huán)境下
發(fā)生內(nèi)存泄漏的兩中情況:
第一是對象被另一個(gè)生命周期特別長的對象所引用
第二是循環(huán)引用中的對象定義了_del_函數(shù)
檢測內(nèi)存泄漏的工具有很多肠阱,這里列舉幾種常見且有用的工具:
- objgraph python2,3下都可使用
- tracemalloc python3下使用
- pympler
objgraph
文檔地址:https://mg.pov.lt/objgraph/
# dot t.dot -T png -o pic.png
count(typename) #返回該類型對象的數(shù)目朴读,其實(shí)就是通過gc.get_objects()拿到所用的對象屹徘,然后統(tǒng)計(jì)指定類型的數(shù)目。
by_type(typename) #返回該類型的對象列表衅金。線上項(xiàng)目噪伊,可以用這個(gè)函數(shù)很方便找到一個(gè)單例對象
show_most_common_types(limits = 10)# 打印實(shí)例最多的前N(limits)個(gè)對象,調(diào)用前,最好先gc.collet一下
show_backrefs() #生成有關(guān)objs的引用圖氮唯,看出看出對象為什么不釋放酥宴。
find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()) #找到一條指向obj對象的最短路徑,且路徑的頭部節(jié)點(diǎn)需要滿足predicate函數(shù) (返回值為True)可以快捷您觉、清晰指出 對象的被引用的情況拙寡,后面會展示這個(gè)函數(shù)的威力
show_chain() # 將find_backref_chain 找到的路徑畫出來。
show_growth 可以看出自上次調(diào)用后琳水,對象的增長情況
# -*- coding:utf-8 -*-
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 創(chuàng)建對象
class L(list):
def __del__(self):
print('list')
def leak():
objgraph.show_growth()
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
leak()
# gc.collect()
print('----------')
objgraph.show_growth()
當(dāng)定位到哪個(gè)對象存在內(nèi)存泄漏肆糕,就可以用show_backrefs查看這個(gè)對象的引用鏈。
# -*- coding:utf-8 -*-
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 創(chuàng)建對象
class L(list):
def __del__(self):
print('list')
def leak():
# objgraph.show_growth()
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
del a,b
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
leak()
gc.collect()
print(gc.garbage)
print('----------')
# objgraph.show_growth()
objgraph.show_backrefs(objgraph.by_type('Test')[0], max_depth = 10, filename = 'pic.png')
# objgraph.show_backrefs(objgraph.by_type('Test')[0], extra_ignore=(id(gc.garbage),), max_depth = 10, filename = 'pic.png')
上圖所示在孝,Test類的對象存在循環(huán)引用诚啃,并且無法用gc清除,因?yàn)檠h(huán)引用對象定義了_del_方法私沮。另外始赎,可以看見gc.garbage(類型是list)也引用了這兩個(gè)對象,原因在于當(dāng)執(zhí)行垃圾回收的時(shí)候,會將定義了del函數(shù)的類實(shí)例(被稱為uncollectable object)放到gc.garbage列表造垛,因此魔招,也可以直接通過查看gc.garbage來找出定義了del的循環(huán)引用。在這里五辽,通過增加extra_ignore來排除gc.garbage的影響办斑。代碼越復(fù)雜,相互之間的引用關(guān)系越多杆逗,show_backrefs越難以看懂乡翅。這個(gè)時(shí)候就可以使用show_chain和find_backref_chain
# -*- coding:utf-8 -*-
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 創(chuàng)建對象
class L(list):
def __del__(self):
print('list')
def leak():
# objgraph.show_growth()
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
leak()
# gc.collect()
print('----------')
# objgraph.show_growth()
# objgraph.show_backrefs(objgraph.by_type('Test')[0], max_depth = 10, filename = 'chain.png')
objgraph.show_chain(
objgraph.find_backref_chain(
objgraph.by_type('Test')[0],
objgraph.is_proper_module
),
filename='chain.png'
)
對象定義了_del_方法,且存在循環(huán)引用罪郊,垃圾回收回收不了蠕蚜。
python2上述代碼經(jīng)測試正常,但是python3報(bào)錯(cuò)悔橄。難道是python3改進(jìn)了波势?待確認(rèn),python3的del加gc回收 是否可以消除循環(huán)引用,答案是Python 3.4以后都可以自動(dòng)處理橄维。
另外尺铣,關(guān)于內(nèi)存泄漏的定位,還可設(shè)置gc為debug模式争舞,打印出不可回收對象凛忿,從而排查出可能發(fā)生內(nèi)存泄漏的對象。
# -*- coding:utf-8 -*-
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 創(chuàng)建對象
class L(list):
def __del__(self):
print('list')
def leak():
objgraph.show_growth()
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
del a,b
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
gc.set_debug(gc.DEBUG_LEAK)
# gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
leak()
print('----------')
objgraph.show_growth()
tracemalloc
tracemalloc 是python3內(nèi)置庫竞川,非常輕量店溢,可以用于追蹤內(nèi)存的使用情況,功能強(qiáng)大委乌,用法也很簡單床牧,遺憾的是python2不支持。https://docs.python.org/3/library/tracemalloc.html
例:
import tracemalloc
tracemalloc.start() # 開始跟蹤內(nèi)存分配
test = [i for i in range(100000)]
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno') # lineno,逐行統(tǒng)計(jì)遭贸;filename戈咳,統(tǒng)計(jì)整個(gè)文件內(nèi)存
for stat in top_stats:
print(stat)
結(jié)果:
/Users/mac/temp/makemoney_admin_flask/app/test.py:5: size=3533 KiB, count=99745, average=36 B
從結(jié)果來看,
文件第5行消耗了3533 KiB的內(nèi)存壕吹。
如果想統(tǒng)計(jì)某段程序的內(nèi)存情況著蛙,可以比較兩段快照之間的內(nèi)存,如下:
import tracemalloc
tracemalloc.start()
# ... start your application ...
snapshot1 = tracemalloc.take_snapshot()
test1 = [i for i in range(100000)]
test2 = [i for i in range(100000)]
# ... call the function leaking memory ...
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[ Top 10 differences ]")
for stat in top_stats[:10]:
print(stat)
結(jié)果 :
/Users/mac/temp/makemoney_admin_flask/app/test.py:8: size=3532 KiB (+3532 KiB), count=99744 (+99744), average=36 B
/Users/mac/temp/makemoney_admin_flask/app/test.py:7: size=3532 KiB (+3532 KiB), count=99744 (+99744), average=36 B
/Users/mac/temp/makemoney_admin_flask/app/test.py:6: size=576 B (+576 B), count=1 (+1), average=576 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:387: size=96 B (+96 B), count=2 (+2), average=48 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:524: size=56 B (+56 B), count=1 (+1), average=56 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:281: size=40 B (+40 B), count=1 (+1), average=40 B
從打印結(jié)果得知耳贬,消耗內(nèi)存的程序段分布踏堡,可以知道哪些代碼消耗內(nèi)存較大,分析具體內(nèi)存泄漏的情況咒劲,非常有用顷蟆。
pympler
也是一種可以排查追蹤內(nèi)存泄漏的工具诫隅,參考文檔https://pythonhosted.org/Pympler/
# -*- coding:utf-8 -*-
from pympler import tracker
tr = tracker.SummaryTracker()
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 創(chuàng)建對象
class L(list):
def __del__(self):
print('list')
def leak():
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
# del a,b
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
# gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
tr.print_diff()
leak()
print('----------')
tr.print_diff()
結(jié)果:
types | # objects | total size
============================ | =========== | ============
list | 3539 | 362.31 KB
str | 4148 | 297.93 KB
dict | 48 | 72.38 KB
code | 267 | 33.38 KB
type | 23 | 20.30 KB
int | 327 | 7.66 KB
_sre.SRE_Pattern | 13 | 6.00 KB
set | 6 | 5.86 KB
weakref | 31 | 2.66 KB
tuple | 39 | 2.36 KB
getset_descriptor | 24 | 1.69 KB
function (__init__) | 13 | 1.52 KB
wrapper_descriptor | 17 | 1.33 KB
builtin_function_or_method | 11 | 792 B
property | 8 | 704 B
----------
types | # objects | total size
======================= | =========== | ============
list | 221 | 20.75 KB
str | 223 | 12.78 KB
dict | 2 | 560 B
<class '__main__.L | 2 | 224 B
int | 8 | 192 B
<class '__main__.Test | 2 | 128 B
主要看print('----------')之后的。
弱引用
弱引用模塊是weakref可以用于消除循環(huán)引用帐偎。
# -*- coding:utf-8 -*-
import time,gc,objgraph
import weakref
class Test(object):
def __del__(self):
print("del is called")
def callback(self):
print("callback")
def leak():
t1 = Test()
t2 = Test()
t1.arrt2 = weakref.proxy(t2)
t2.arrt1 = weakref.proxy(t1)
leak()
gc.collect()
print(gc.garbage)