Python是如何管理內(nèi)存的?
1. 內(nèi)存管理關(guān)我屁事?
內(nèi)存管理是指在程序的運行過程中,分配內(nèi)容和回收內(nèi)存的過程。如果只分配,不回收,電腦上那點內(nèi)存很快就被用光。
好的程序能夠高效的使用內(nèi)存,不好的程序會造成過多的內(nèi)存消耗,內(nèi)存泄露,棧溢出,程序死翹翹。

幸運的是,Python和Java等高級語言會自動管理內(nèi)存的分配和回收。
但程序員仍然必須具有一定內(nèi)存管理知識!
小白需要內(nèi)存管理知識避免犯低級錯誤,高手需要內(nèi)容管理知識來優(yōu)化程序性能,就像賽車手調(diào)教汽車的性能。

舉個例子:生成包含1億個隨機字符串的序列,小白可能用list,而有點經(jīng)驗的會用generator。內(nèi)存的使用效率可能差了一億倍。
generator出現(xiàn)的一個重要原因就是省內(nèi)存。
2. 可變數(shù)據(jù)類型和不可變數(shù)據(jù)類型
當(dāng)我們調(diào)用函數(shù)的時候,我們需要傳遞參數(shù),這時候變量從一個函數(shù)傳遞到了另一個函數(shù)。這個傳遞過程發(fā)生了什么?
被傳遞的對象是被復(fù)制了一份呢?還是就是同一份呢?
我們得先理解變量的存儲結(jié)構(gòu),尤其是list等容器類的存儲。
看下面的代碼:
name?=?'麥?zhǔn)?
print(id(name))??#打印內(nèi)存地址:140628480727248
name?=?'張三'
print(id(name))??#打印內(nèi)存地址:140628480727056
當(dāng)我們定義了一個變量name = '麥?zhǔn)?,在內(nèi)存中有兩個部分:
一個是name這個變量名 一個是真正存儲'麥?zhǔn)?這個字符串的對象
上面的代碼在內(nèi)存中的過程是這樣的:
開始變量名name指向了“麥?zhǔn)濉薄?/section> 后來變量名name指向了“張三”。
注意:這時候“麥?zhǔn)濉辈辉儆凶兞渴褂昧?,可能會被垃圾回收器銷毀掉。

對于一個復(fù)雜的數(shù)據(jù)類型,比如list,原理是相同的但是更復(fù)雜:
cities?=?['北京',?'上海',?'廣州',?'深圳']
print(id(cities))
cities[3]='雄安'
print(id(cities))

cities一直指向內(nèi)存中的列表對象,列表中的值改變不會改變cities在內(nèi)存中的地址。改變的是列表指向的另外一個對象的地址。
這里的要點:
name等字符串是不可變的,當(dāng)變量的值變化了,實際上生成了一個新的變量,內(nèi)存地址變化了。
基本數(shù)據(jù)類型都是不可變的,還有整數(shù),小數(shù)等都是不可變。當(dāng)一個變量的值發(fā)生了變化,實際上是創(chuàng)建了一個新的對象。
而cities是個list,是可變的,里面的值會發(fā)生變化,內(nèi)存地址沒有發(fā)生變化??勺冞€包括dict等。
3.參數(shù)傳遞
理解了變量的內(nèi)存存儲結(jié)構(gòu),來看參數(shù)傳遞的問題,在函數(shù)調(diào)用過程中,參數(shù)傳遞到底是傳遞了什么?是復(fù)制了一份嗎?
簡單說:函數(shù)參數(shù)和返回值的傳遞都是傳遞的變量指向的內(nèi)存地址!
import?random
name?=?'麥?zhǔn)?
print(f'name的地址:{id(name)}')
?
def?shuai_score(person):
????print(f'name的地址:{id(person)}')
????score?=?random.randint(1,?10)
????print(f'score的地址:{id(score)}')
????return?score
?
mscore?=?shuai_score(name)
print(f'mscore的地址:{id(mscore)}')
對于普通對象(不可變類型)和容器類變量可能看起來效果不一樣,但實際上原理是一樣的。如下面的代碼:
全局變量name指向張三,這一直都沒有變過。 局部變量name本來是指向張三的,但是后來指向了李四,這并不影響全局變量還是指向張三。
name?=?'zhangsan'
?
def?hello(name):
????print('傳進來的是:'+?name)
????name?=?'lisi'
????print('name被改成了'?+?name)
?
print(name)
對于列表,改變列表的值會影響全局變量。
cities?=?['北京',?'上海',?'廣州',?'深圳']
print(f'全局cities的地址:{id(cities)}')
?
def?change_city(cities):
????print(f'局部cities的地址:{id(cities)}')
????cities[0]?=?'雄安'
????print(f'局部cities的地址:{id(cities)}')
?
change_city(cities)
print(cities)
print(f'全局cities的地址:{id(cities)}')
4. 引用次數(shù)和垃圾回收器
上面的知識和內(nèi)存管理有什么關(guān)系?當(dāng)然有!
內(nèi)存管理的基本原理:回收掉沒用的內(nèi)存!
怎么判定有用沒用呢:如果還有變量在使用就不要回收,沒變量使用了就干掉它。
好可怕,做個打工人也一樣吧?如果你還有用就繼續(xù)打工,沒用了就干掉。
上面的例子中:'麥?zhǔn)?這個對象沒用了,因為變量name指向了新的對象'張三'。
Python用引用次數(shù),英文叫做reference count來表示有幾個變量在使用這個對象。
通過一個叫做垃圾回收器(英文是Garbage Collector)的后臺線程定期查看是否有些變量的引用次數(shù)為0,并清理掉引用次數(shù)為0的對象。

引用次數(shù)是如何產(chǎn)生的?
當(dāng)對象被賦值給新的變量,引用次數(shù)就會增加 當(dāng)變量作為參數(shù)傳遞給其他函數(shù),引用次數(shù)就會增加
引用次數(shù)如何減少?
當(dāng)函數(shù)執(zhí)行結(jié)束,引用對象的變量不再有效,引用次數(shù)減少
我們可以通過sys.getrefcount查看一個對象的引用次數(shù):
import?sys
name?=?'麥?zhǔn)?
print(sys.getrefcount(name))?#打印4
上面的代碼會打印出4次!
這是怎么回事?明明只有一次??!這4個引用是:
name變量 getrefcount:當(dāng)name被傳遞給getrefcount函數(shù)的時候,函數(shù)的參數(shù)也指向了它。 Python解釋器:為了執(zhí)行這個腳本,Python解釋器也保留了一個引用,直到腳本結(jié)束。只針對腳本全局變量。 編譯優(yōu)化器:當(dāng)執(zhí)行腳本的時候,優(yōu)化器會嘗試優(yōu)化字節(jié)碼,所以也產(chǎn)生了一次引用。這個引用是臨時的,很快就會消失。
如果不在腳本中運行,直接在交互式Python下運行,refcount是2,因為沒有后面兩個引用:
>>>?import?sys
>>>?name?=?'麥?zhǔn)?
>>>?print(sys.getrefcount(name))??#打印2
再來看一段代碼:
import?sys
name?=?'麥?zhǔn)?
print(sys.getrefcount(name))??#打印4
name2?=?name
print(sys.getrefcount(name))??#打印5
name3?=?name?
print(sys.getrefcount(name))??#打印6
因為name2和name3都指向了'麥?zhǔn)?,所以引用次數(shù)不斷增加。
注意:getrefcount函數(shù)執(zhí)行完,它產(chǎn)生的引用就消失了,所以不會因為調(diào)用了3次而增加3個。
5. 手工回收
正常情況下,回收垃圾這事兒都是Python的垃圾回收器的活。
就像賽車手要自己調(diào)教汽車,必要的時候我們也可以自己動手。
import?sys,?gc
#?這個函數(shù)創(chuàng)建一個自己指向自己的列表
def?create_cycle():
????list?=?[8,?9,?10]
????list.append(list)
print("創(chuàng)建垃圾...")
for?i?in?range(8):
????create_cycle()
print("我們來強制回收...")
n?=?gc.collect()
print("清理掉的無頭尸體:",?n)
print("沒清理的垃圾:",?gc.garbage)
執(zhí)行結(jié)果如下:
創(chuàng)建垃圾...
因為與引用,所以不會被自動回收...
我們來強制回收...
清理掉的無頭尸體:?8
沒清理的垃圾:?[]
運送垃圾的車有自己的時間點,比如每天早上6點。但如果垃圾太多了,也可以打電話讓他們馬上來過清理垃圾。
垃圾回收器有它自己的運行節(jié)奏。我們可以調(diào)用gc.collect()讓它馬上執(zhí)行回收操作。
6. 要點
知道可變數(shù)據(jù)類型,不可變數(shù)據(jù)類型,以及參數(shù)傳遞原理 理解list等可變數(shù)據(jù)類型作為參數(shù)傳遞后修改的是同一個對象 合理使用對象,避免占用太多內(nèi)存,比如大數(shù)據(jù)情況下使用generator而不是list 避免變量循環(huán)引用,造成引用數(shù)永遠不為零,可能造成不可回收而引起內(nèi)存泄露。
今天這些對大部分人可能夠用了。其實內(nèi)存管理和垃圾回收是比較高級的話題,屬于編程的深水區(qū)。有興趣的建議多研究一下,深水區(qū)里去游一下。
