什么是垃圾?
當我們的Python/ target=_blank class=infotextkey>Python解釋器在執行到定義變量的語法時,會申請內存空間來存放變量的值,而內存的容量是有限的,這就涉及到變量值所占用內存空間的回收問題。那么什么是垃圾呢?簡單來說垃圾就是指:當一個對象或者說變量沒有用了,這時候它就是垃圾了。
a = 10000 # 開辟內存存儲值
a = 30000 # 開辟新內存存儲值
# 此時10000沒有被引用了,變成垃圾,需要被回收內存。
內存泄漏與內存溢出
上面的代碼看到有”垃圾“產生了,如果不處理咋辦?舉個例子:
-
你廁所拉屎不沖水,越拉越多,然后有一天,你又想拉,發現廁所堵了,沒地拉屎
-
你覺得房間還空曠,繼續拉屎,慢慢地你房間滿了,你也就被屎山覆蓋完蛋了。
上面的結果換句話說就是下面:
內存泄漏:程序在申請內存后,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積后果很嚴重,無論多少內存,遲早會被占光
內存溢出:程序在申請內存時,沒有足夠的內存空間供其使用,出現 out of memory
數據池和緩存:
-
小整數池:Python會自動緩存范圍在[-5, 256]內的整數對象,避免頻繁創建和銷毀對象。當多個變量引用相同的整數對象時,它們指向同一個內存地址的同一個對象。
-
intern機制:intern機制用于緩存并重復使用一些字符串對象,以減少內存開銷和提高性能。當Python解釋器遇到字符串字面量時,會先檢查該字符串是否存在于intern字符串池中。如果存在,則直接返回對應字符串對象的引用;否則,創建一個新的字符串對象,并將其添加到intern字符串池中,以便后續重復使用。
import sys
# 創建兩個相同值的字符串對象
str1 = "hello"
str2 = "hello"
# 檢查兩個字符串對象的內存地址
print(id(str1),id(str2))
# 使用 intern 機制將字符串添加到 intern 字符串池中
str1 = sys.intern(str1)
str2 = sys.intern(str2)
# 再次檢查兩個字符串對象的內存地址
print(id(str1),id(str2))
-
其他緩存機制:Python還使用了其他一些緩存機制,例如常用的空元組、空字典和空集合會被緩存以減少內存開銷。
這些數據池和緩存機制可以在一定程度上優化Python的內存使用,特別是對于頻繁使用的對象類型或值。
垃圾回收
Python的垃圾回收機制用于解決循環引用和內存泄漏的問題。當一個對象不再被引用時,其占用的內存應該被回收以供其他對象使用。python采用的是引用計數機制為主,標記-清除和分代收集(隔代回收)兩種機制為輔的策略
一、引用計數
Python的引用計數機制是內存管理的基礎。當對象被創建時,Python會分配內存地址,并對對象進行初始化。所有對象都會維護在一個雙向循環鏈表中,該鏈表稱為refchAIn。每個對象都保存以下信息:
-
鏈表中指向前后對象的指針
-
對象的類型
-
對象的值
-
對象的引用計數
-
對象的長度(適用于list、dict等可變對象)
引用計數增加的情況包括:對象被創建、對象被其他變量引用、對象作為容器的元素、對象作為參數傳遞到函數中。
引用計數減少的情況包括:對象的別名被顯式銷毀、對象的別名被賦值給其他對象、對象從容器中被移除或容器被銷毀、引用離開其作用域。
我們可以使用sys模塊中的getrefcount()
函數來查看對象的引用計數。需要注意的是,getrefcount()
函數會在內部臨時增加對象的引用計數,所以需要減去函數本身的引用計數才能得到對象的真實引用計數。
import sys
# 引用計數
numbers = [1, 2, 3]
ref_count = sys.getrefcount(numbers) - 1
print(ref_count) # 輸出 1
# 循環引用:對象互相引用
a = [1, 2]
b = [2, 3]
a.Append(b) # 計數2
b.append(a) # 計數2
del a # 計數1
del b # 計數1
# 引用自身
c = [1, 2, 3, 4]
c.append(c)
當一個對象的引用計數為0時,表示沒有任何變量引用該對象,可以被回收。但引用計數機制無法解決循環引用的問題。
引用計數的缺點:1. 循環引用 2. 維護引用計數消耗資源
二、標記-清除策略
標記-清除算法:用于解決無法通過引用計數回收的垃圾對象的算法。它的基本思想是從根對象開始,通過標記所有可以訪問到的對象,然后清除未標記的對象。清除階段會釋放未被引用的對象占用的內存。
該策略在回收垃圾的時候有兩步:
標記階段:
在此階段,垃圾回收器會從一組根對象開始,例如全局變量、活動的函數調用棧和特殊的內部數據結構。這些根對象是我們確定的起始點。垃圾回收器會遞歸地遍歷這些根對象,并標記所有可以從根對象訪問到的對象。它會使用一種標記機制,通常是在對象的內存布局中添加一個額外的標記位來表示對象的狀態。初始狀態下,所有對象的標記位都是未標記的。通過遍歷對象之間的引用關系,從根對象開始,垃圾回收器會標記所有可達的對象。如果一個對象被標記,則意味著它是活動的,即仍然被程序使用。未被標記的對象則被認為是垃圾對象。
清除階段:
垃圾回收器會掃描整個堆內存,并清除所有未被標記的可達對象。這些未被標記的可達對象被認為是垃圾,因為它們不再被程序使用。
標記-清除是一種周期策略,每隔一段時間進行一次掃描,這種過程會暫停整個應用程序,等待標記-清除結束后才會恢復應用的運行。
分代回收策略
因為標記-清除策略會讓程序阻塞,為了提高垃圾回收的效率,在標記-清除的基礎上進一步建立分代回收,是一種空間換時間提高回收效率的策略。
Python將內存劃分為不同的代(generation),包括年輕代(第0代)、中年代(第1代)和老年代(第2代)。這樣劃分的目的是根據對象的存活時間來優化垃圾回收的效率。
通常情況下,年輕代的對象存活時間較短,而老年代的對象存活時間較長。因此,Python的分代回收策略認為,存活時間較長的對象越來越不可能是垃圾,所以在回收時應該更少地去檢查老年代的對象。
觸發分代回收的條件由閾值控制。
import gc
print(gc.get_threshold()) # 默認策略閾值:(700,10,10)
# 閾值設置為(700, 10, 10),表示當分配對象的個數達到700時,
# 進行一次0代回收;經過10次0代回收后,觸發一次1代回收;
# 經過10次1代回收后,觸發一次2代回收。
gc.set_threshold(500, 5, 5) # 自己設置回收策略閾值
將對象分為不同的代(Generation),每個代有不同的回收頻率。新創建的對象屬于第0代(掃描算法),經過一次垃圾回收仍存活的對象會晉升到下一代,而不活躍的對象會被更頻繁地回收。
思考
-
什么時候釋放不再使用的對象?
可以通過顯式地將對象設置為None
或使用del
關鍵字來解除對象的引用。 -
如何避免循環引用?
避免循環引用的方法包括使用弱引用或重新設計數據結構以消除循環引用。 -
如何減少內存占用?
對于需要頻繁創建和銷毀的對象,可以考慮使用緩存, 避免重復創建相同的對象。當然,咱們搞垃圾系統的,知道那么多這種干啥呢?用python不就是想著easy嘛?你怎么老想著性能呢?