簡述Python垃圾回收機制和常量池的驗證

經過代碼驗證python解釋器內部使用了常量池

Python的引入

人類認識世界是從認識世界中的一個又一個實物開始,而後再對其用語言加以描述。例如當中國人看到蘋果時,便會用中文「蘋果」加以描述,而用英語的一些國家則會用「apple」加以描述。python

以上說到的中文和英文都是人類認識並描述世界的一個工具,而在計算機的世界中,爲了讓計算機去認知世界,從而幫助人類完成更多的任務。在計算機領域中也發展了語言這個工具,從早期的機器語言到彙編語言再到如今使用範圍較廣的高級語言。而咱們接下來要介紹的Python則屬於高級語言這一分支。app

變量的引入

爲何要有變量

上面說到Python是計算機世界中用來描述外部世界的,而且也說起了世界就是一個又一個實物的堆疊,描述世界其實就是去描述那一個又一個實物,人類如此,計算機也是如此。所以計算機語言開發者們爲了使用計算機語言的人更好的在計算機中去描述這些實物,便在計算機語言中引入了變量這個概念,Python也不例外。簡單點說,變量就是用來描述世間萬物的。函數

定義變量

爲了在計算機書寫方便,定義一變量也有必定的規則,在這裏咱們僅說說Python中變量的定義規則,首先咱們先定義兩個變量:工具

name = 'chenyoude'
year = 2021

上述代碼中咱們便定義了兩個變量,從上面定義的兩個變量中,咱們能夠看到,變量的組成分爲三個部分:編碼

  1. 變量名:反應變量值所描述的意義,而且能夠用來引用變量值。
  2. 賦值符號:賦值。
  3. 變量值:存放數據,用來記錄現實世界中的某種狀態。

常量引入

上面簡單講解了Python中的變量,經過字面意思,能夠看到變量實際上是一個變化的量,例如,下面這個實例:翻譯

year = 2021
year = year + 1
print(year) # 輸出結果:2022

剛開始咱們賦予了year一個變量值爲2021,當咱們對year進行加1操做時,能夠發現year值變成了2022。對於上述現象咱們不難理解,由於以前說過Python中變量是用來描述世間萬物的,世間萬物在現實中是能夠變化的,變量固然也能夠隨之變化。設計

可是在某個局部範圍內,變量多是不會變化的,例如在2021年這一年,都只會是2021年,沒有人會說2021年是2022年。若是你有豐富的開發經驗,會明白變量定義出來不是存放在那裏給你看的,更多的是要拿來用的。也就是說若是在2021年中的某個程序須要使用year這個變量,但這個變量是不須要進行修改的。爲了防止誤操做對year這個變量進行了修改,計算機語言便設計了常量這個概念,也就是說常量相對於變量是一個不會變化的量。code

在Python中,有沒有常量呢?不嚴格的講,實際上是有的,只是在定義常量的時候常量名必須的全大寫,例如,下面這個實例:內存

YEAR = 2021
YEAR = YEAR + 1
print(YEAR) # 輸出結果:2022

上面這個常量的實例使人大吃一驚,由於使用常量YEAR後和使用變量year的結果一致,也就是說常量YEAR遭到了更改。可是,稍微解釋你就明白了。作用域

在Python中,雖然也和其餘不少計算機語言同樣擁有常量這個概念,但更多的是約定俗成的,Python並無嚴格的對常量進行控制,只是規定常量名必須所有大寫。緣由很簡單:都是常量了,你爲何還要修改?

常量池引入

上面講到常量就是一個不會變化的變量,嚴格的講,在Python中是沒有常量這個概念的。可是,在Python中又有另一種例外,那就是常量池,爲了搞清楚常量池,首先咱們得弄明白Python的幾個小知識,接下來一一敘說。

Python解釋器

上面說起到Python是計算機用來描述世間萬物的一種語言,因爲計算機沒有人腦那麼強大,計算機更多的只是認識高低壓電頻,再經過對高低壓電頻的轉化進而編碼成咱們看到的一個又一個字符,也就是說計算機是沒法直接認識利用Python寫下的字符的。(此處設計計算機組成原理,很少作介紹)

也就是說,當咱們利用Python寫下一個又一個字符而且交給電腦時,須要經過編碼這個過程,而這個編碼的過程有時候也被稱爲解釋。解釋的原理就至關於從中文轉成英文,只不過此時不是須要讓英文使用者看懂中文,而是讓計算機可以看懂Python。

中文轉成英文的時候,可能須要一個翻譯員或一個翻譯軟件,利用Python寫下的字符轉化爲計算機能看懂的語言一樣如此,這個轉化過程也須要一個外物的幫助——Python解釋器。

Python變量存儲機制

假設咱們使用Python解釋器定義瞭如下一個變量:

year = 2021

當咱們經過字符定義變量時,必定會好奇這些變量被Python解釋器解釋後到底去了哪?若是對計算機的組成熟悉的同窗,必定會清楚計算機的核心組件爲:CPU、內存、外存、輸入設備、輸出設備。也就是說,這些字符應該存儲在這些核心組件中。在這裏就不賣關子了,當咱們經過字符定義變量並對其用Python解釋器進行解釋時,他們會以計算機能看懂的形式進入內存當中。

上面講的對於不少非科班出身的朋友可能很難理解,在這裏將它生動化。如今假設江西師範大學至關於電腦內存,每當有一批新學生進入師大時,師大都會開闢出一個新教室給這批新同窗使用,而且會給每個教室一個獨一無二的教室牌號。因爲把師大看做是內存,這批新同窗就能夠當作是變量值,而教室牌號就是變量名。也就是說,對於師大這個大內存,每定義一個變量year=2021,就會在這個大內存中開闢一個小空間,小空間中放變量值2021,而後大內存會給這個小空間定義一個變量名year,此時變量名year指向變量值2021。

上面說到每當Python解釋器解釋一個變量時,會將這個變量存放到內存中的一個小空間中,但如何知道這個小空間的具體位置呢?此處介紹Python的一個內置函數id(),經過這個函數能夠獲取某一個變量所在的內存地址,例以下面這個實例:

year = 2021
print(id(year)) # 輸出4499932432

Python垃圾回收機制

對於上述師大的例子,此處再作延伸。因爲那一批學生所在班級新轉來了幾位同窗,須要那一批學生更換更大一點教室,也就是給他們一個新的教室。那麼學校應該會這樣處理,首先開闢一個新的教室,而後拿下那一批學生原有教室的教室牌號更換到這個新教室,最後會清空原有教室。

在Python中,也是如此,若是到了新的一年,咱們會從新定義一個year變量,也就是year=2022。若是這是在同一個程序中如此作,Python會沿用上述更換教室的方法,它首先會解除year和2021的鏈接,開闢一個新內存存放變量值2022,讓year與2022鏈接。此時,會發現2021這個變量值只有變量值而沒有變量名,所以這個沒有變量名的變量值會變成Python眼中的一個垃圾變量,從而觸發Python垃圾回收機制,對這個2021所在的內存空間進行回收。

爲了更好地理解Python垃圾回收機制,能夠看下面這個例子:

year = 2021
print(id(year)) # 輸出4499932720
print(year) # 輸出2021

year = 2022
print(id(year)) # 輸出4499932560
print(year) # 輸出2022

經過上述例子,能夠看到當新定義了一個year變量時,year會與新的變量進行一個鏈接。固然,此處所說的垃圾回收機制只是爲了引入引用計數這個概念,並非徹底正確的解釋,而且上述實例還沒法證實變量值2021所在內存是否被回收,下面將經過引用計數的實例會進一步說明並從新解釋垃圾回收機制。

引用計數

上面講到若是某個變量值綁定着變量名,就是一個正常的變量,若是該變量值沒有綁定着門牌號,這個變量就是一個垃圾變量,對於垃圾變量,Python會觸發垃圾回收機制回收這個變量所佔有的內存。進而能夠想到,Python中一個變量名必定只能對應一個變量值。

在這裏咱們就不能沿用師大這個例子了,而得引出一個新的名詞——引用計數。

爲了解釋引用計數,咱們首先得明白在Python中,當定義了一個變量值爲2021的變量時,它能夠表示年份、也能夠表示山的高度…也就是說一個變量名只能對應一個變量值,可是一個變量值能夠對應不一樣的變量名,這種設計也是比較合理的。

如今咱們引出引用計數這個概念,當相同的變量值被賦予不一樣的變量名時,變量值每增長一個變量名的賦予,則該變量值的引用計數加1。因爲咱們能夠經過Python內置sys模塊中的getrefcount()函數獲取某一個變量的引用計數(getrefcount輸出值默認從3開始),能夠經過下面這個例子感覺下:

import sys

# 引用計數初始值爲3
print(sys.getrefcount(2021)) # 輸出爲3

year = 2021
print(sys.getrefcount(2021)) # 輸出爲4

height = 2021
print(sys.getrefcount(2021)) # 輸出爲5

del year
print(sys.getrefcount(2021)) # 輸出爲4

從上述代碼能夠看出變量值2021的引用計數因爲每一次賦予新的變量名,引用計數都會增長,而當咱們利用del關鍵字刪除變量值2021的一個變量名year時,引用計數則會減小。

爲了更加嚴謹的表達引用計數,此處不得再也不次深刻,引用計數字面意思能夠理解爲引用的次數,也就是說上面的例子其實並不嚴謹,更嚴謹的講,只有當一個變量值每一次被直接或間接引用時,引用計數纔會增長,在Python中讓引用計數增長共有三種方法:

  1. 變量被建立,變量值引用計數加1
  2. 變量被引用,變量值引用計數加1
  3. 變量做爲參數傳入到一個函數,變量值引用計數加2

具體看下述實例:

import sys

# 引用計數初始值爲3
print(sys.getrefcount(2021)) # 輸出爲3

# 變量被建立,變量值引用計數加1
year = 2021
print(sys.getrefcount(2021)) # 輸出爲4

# 變量被引用,變量值引用計數加1
height = year
print(sys.getrefcount(2021)) # 輸出爲5

# 變量做爲參數傳入到一個函數,變量值引用計數加2
def func(year):
    print(sys.getrefcount(year)) 

func(year) # 輸出爲7

Python中既然有增長引用計數的方法, 也固然會減小引用計數的方法,共有如下4種:

  1. 變量值對應的變量名被銷燬
  2. 變量值對應的變量名被賦予新的值
  3. 變量值對應的變量名離開它的做用域
  4. 變量值對應的變量名的容器被銷燬

重看Python垃圾回收機制

有了getrefcount()方法並經過引用計數,咱們就能夠解開垃圾回收機制遺留的一個問題——如何判斷是否觸發了垃圾回收機制。每當一個變量定義,他的getrefcount輸出值爲3,而若是該變量值被垃圾回收機制回收,則它的getrefcount輸出值回到3,能夠經過下面實例驗證上述猜測:

import sys

print(sys.getrefcount(2021)) # 輸出爲3

year = 2021
print(sys.getrefcount(2021)) # 輸出爲4
print(id(year)) # 輸出4499932720
print(year) # 輸出2021

year = 2022
print(sys.getrefcount(2021)) # 輸出爲3
print(id(year)) # 輸出4499932560
print(year) # 輸出2022

經過上述實例,能夠發現因爲變量值2021對應的變量名被新的變量值2022引用,它的getrefcount輸出值爲3,引用計數變成了0,所以能夠證實Python觸發了垃圾回收機制。

若是對上述驗證Python觸發垃圾回收機制的實例深刻挖掘,會發現當把year賦給變量值2022時,變量值的2021的引用計數爲0,此時觸發了Python的垃圾回收機制,那麼是否能夠代表只有當變量值2021的引用計數爲0時才能觸發垃圾回收機制呢?而不是上一次說的當變量值的變量名被新的變量值被引用了纔會銷燬呢?由於變量值能夠對應多個變量名,下面經過下述實例驗證:

import sys

print(sys.getrefcount(2021)) # 輸出爲3

year = 2021
print(sys.getrefcount(2021)) # 輸出爲4

height = 2021
print(sys.getrefcount(2021)) # 輸出爲5

year = 2022
print(sys.getrefcount(2021)) # 輸出爲4

del height
print(sys.getrefcount(2021)) # 輸出爲3

經過上述實例,能夠發現因爲定義一個變量後,該變量對應的變量值引用計數能夠不斷增長,而只要引用計數不爲0,那麼Python就一直還在內存中保留着這個變量值而且對其引用,只有當該變量的引用計數爲0時,Python纔會觸發垃圾回收機制對該變量值進行回收,這纔是比較正確的垃圾回收機制。固然,若是深刻,Python的回收機制還有分代回收,此處不作延展,瞭解上述這些就足矣瞭解接下來說的小整數池。

常量池

在上述各個知識的打通以後,如今能夠正式引入常量池這個概念。上面講到在Python中嚴格的講是沒有常量這個概念的,即便你經過約定俗成的方法定義了一個常量,但這個常量也只是一個變量,也就是說只要你對這個常量作出修改,這個常量原有對應的常量值引用計數就會變成0,因爲常量等同於變量,它同樣會被Python垃圾回收機制回收。

可是在Python中,存在着一些例外,這些例外就是一個小整數池,顧名思義,小整數池表示的是從-5到256範圍內的整數,這些整數定義出來後就是一個常量,也就是說他們的引用計數即便爲0,也不會被Python的垃圾回收機制回收,能夠經過下述實例驗證:

import sys

first_l = []  # 定義列表l存儲[-5,256]中的全部整數的引用計數
add_l = []  # 定義列表add_l存儲[-5,256]中的全部整數的引用計數加1後的引用計數
del_l = []  # 定義列表del_l存儲[-5,256]中的全部整數的引用計數減1後的引用計數

for i in range(-5, 256):
    first_l.append(sys.getrefcount(i))
    add = i
    add_l.append(sys.getrefcount(i))
    del add
    del_l.append(sys.getrefcount(i))

first_l.sort()
add_l.sort()
del_l.sort()

print(f'min(first_l): {min(first_l)}') # 獲取[-5,256]中全部整數的最小引用計數,輸出爲4
print(f'min(add_l): {min(add_l)}') # 獲取[-5,256]中全部整數的最小引用計數,輸出爲5
print(f'min(del_l): {min(del_l)}') # 獲取[-5,256]中全部整數的最小引用計數,輸出爲4

從上述實例能夠看出,[-5,256]中的整數的getrefcount默認初始值爲4,也就是說即便沒有對這些整數進行初始化的建立,Python早已對他們進行了引用,即便他們的引用計數爲0,他們也不會也不可能被刪除,由於他們從Python解釋器啓動開始就已經被生成。

固然,也能夠經過垃圾回收機制判斷小整數池中的整數是否會被垃圾回收機制回收,可用以下實例證實(因爲Pycharm等解釋器會一次性編譯整個文件,固使用終端編輯代碼):

>>> a = 5
>>> id(a)
4529334480
>>> del a
>>> b = 5
>>> id(b)
4529334480
>>>
>>> a = 257
>>> id(a)
4533920752
>>> del a
>>> b = 257 # 消除分代回收對結果的影響
>>> del b
>>> b = 257
>>> id(b)
4531031792
>>>

從上述實例中能夠看出,變量值5即便被垃圾回收機制回收後,再次建立變量值爲5的變量,該變量的內存地址始終無變化,即該變量未被垃圾回收機制回收,小整數池中的其餘整數同理;而變量值257卻已經被垃圾回收機制回收,非小整數池中的其餘變量同理。

固然,還能夠經過下述方法查看這些小整數池的整數的內存地址的變化,以下:

a = 256
b = int("256")
print(id(a), id(b))  # 4544968752 4544968752

a = 257
b = int("257")
print(id(a), id(b))  # 4548719792 4546289360

a = -5
b = int("-5")
print(id(a), id(b))  # 4544960400 4544960400

a = -6
b = int("-6")
print(id(a), id(b))  # 4690036912 4546289360

對於上述實例,在Python中,因爲每生成一個變量便會開闢一個新的內存空間給該變量,可是上述實例代表當變量值爲-5和256時,每次開闢的內存空間地址都是同樣的;而當變量值不屬於[-5,256]時,每次定義變量值時,內存空間的地址都是不同的。

總結

在Python中,變量是用來描述世間萬物的,變量顧名思義是變化的一個量,而在某一個局部範圍內,有些量多是不會變化的,所以語言設計者在計算機中定義了常量這個概念,可是在Python中並無規定的常量,只有約定俗稱的常量,也就是變量名全大寫的則是常量。可是Python中有一個另外,也就是小整數池[-5,256],在這個小整數池中的整數對於Python來講就是一個常量,由於從引用計數的打印中能夠看出它在Python解釋器啓動的時候就已經生成並佔用了一個固定的內存空間,而且不會由於引用計數變爲0以後就會被Python的垃圾回收機制回收,而這些小整數池也能夠稱做Python的常量池。

相關文章
相關標籤/搜索