物件生命週期¶
本節解釋了物件在其整個生命週期中,一個型別的槽位(slots)之間如何相互關聯。它並非旨在作為槽位的完整規範參考;相反,有關特定槽位的詳細資訊,請參閱型別物件結構中的槽位特定文件。
生命事件¶
下圖展示了一個物件在其生命週期中可能發生的事件順序。從 A 到 B 的箭頭表示事件 B 可以在事件 A 發生後發生,箭頭的標籤表示 A 發生後 B 發生的條件。
說明
透過呼叫物件的型別構造新物件時
tp_init
完成後,物件即可使用。在最後一個對物件的引用被移除一段時間後
如果一個物件沒有被標記為 finalized,它可能會透過標記為 finalized 並呼叫其
tp_finalize
函式來終結。Python 不 會在對物件的最後一個引用被刪除時終結物件;請使用PyObject_CallFinalizerFromDealloc()
來確保tp_finalize
總是被呼叫。如果物件被標記為已終結,垃圾回收器可能會呼叫
tp_clear
來清除物件持有的引用。當物件的引用計數達到零時,它不會被呼叫。tp_dealloc
被呼叫以銷燬物件。為了避免程式碼重複,tp_dealloc
通常會呼叫tp_clear
來釋放物件的引用。當
tp_dealloc
完成物件銷燬後,它直接呼叫tp_free
(通常根據型別自動設定為PyObject_Free()
或PyObject_GC_Del()
)來釋放記憶體。
如果需要,
tp_finalize
函式允許向物件新增引用。如果它這樣做了,物件就會被 復活,從而阻止其即將到來的銷燬。(只有tp_finalize
允許復活物件;tp_clear
和tp_dealloc
在不呼叫tp_finalize
的情況下不能復活物件。)復活物件可能會或可能不會導致物件的 finalized 標記被移除。目前,如果復活的物件支援垃圾回收(即設定了Py_TPFLAGS_HAVE_GC
標誌),Python 不會從復活的物件中移除 finalized 標記,但如果物件不支援垃圾回收,則會移除該標記;這兩種行為中的一種或兩種都可能在未來發生變化。tp_dealloc
可以選擇透過PyObject_CallFinalizerFromDealloc()
呼叫tp_finalize
,如果它希望重用該程式碼來幫助物件銷燬。這是推薦的做法,因為它保證tp_finalize
總是先於銷燬被呼叫。有關示例程式碼,請參閱tp_dealloc
文件。如果物件是迴圈隔離體的成員,並且
tp_clear
未能打破引用迴圈,或者迴圈隔離體未被檢測到(可能是呼叫了gc.disable()
,或者在其中一個相關型別中錯誤地省略了Py_TPFLAGS_HAVE_GC
標誌),則這些物件將無限期地無法回收(它們“洩漏”)。請參閱gc.garbage
。
如果物件被標記為支援垃圾回收(tp_flags
中設定了Py_TPFLAGS_HAVE_GC
標誌),則以下事件也可能發生
垃圾回收器偶爾會呼叫
tp_traverse
來識別 迴圈隔離體。當垃圾回收器發現一個迴圈隔離體時,它會透過將其標記為 finalized 並呼叫其
tp_finalize
函式(如果存在)來終結組中的一個物件。這會重複進行,直到迴圈隔離體不存在或所有物件都已終結。tp_finalize
允許透過從 迴圈隔離體 外部新增引用來複活物件。新引用導致物件組不再形成迴圈隔離體(引用迴圈可能仍然存在,但如果存在,物件不再隔離)。當垃圾回收器發現一個迴圈隔離體,並且組中所有物件都已被標記為 finalized 時,垃圾回收器會透過呼叫組中一個或多個未清除物件的
tp_clear
函式來清除它們(可能併發進行)。只要迴圈隔離體仍然存在並且並非所有物件都已被清除,此過程就會重複。
迴圈隔離體銷燬¶
下面列出了一個假設的迴圈隔離體的生命階段,該隔離體在每個成員物件被終結或清除後仍然存在。如果一個迴圈隔離體經歷所有這些階段,它就會發生記憶體洩漏;一旦所有物件被清除,它就應該消失,如果不是更早的話。迴圈隔離體消失的原因可能是引用迴圈被打破,或者因為終結器復活導致物件不再隔離(參見tp_finalize
)。
可達(尚未成為迴圈隔離體):所有物件都處於其正常的、可達狀態。可能存在引用迴圈,但外部引用意味著物件尚未被隔離。
不可達但一致: 對一組迴圈物件外部的最後一個引用已被移除,導致這些物件變得隔離(因此誕生了一個迴圈隔離體)。該組中的任何物件都尚未被終結或清除。迴圈隔離體將保持此階段,直到垃圾回收器的未來某個執行(不一定是下一次執行,因為下一次執行可能不會掃描每個物件)。
已終結和未終結混合: 迴圈隔離體中的物件是逐個終結的,這意味著在一段時間內,迴圈隔離體由已終結和未終結物件混合組成。終結順序未指定,因此它可能看起來是隨機的。已終結物件在未終結物件與其互動時必須以合理的方式行為,並且未終結物件必須能夠容忍其引用物件中任意子集的終結。
全部終結: 迴圈隔離體中的所有物件都在其中任何一個被清除之前被終結。
已終結和已清除混合: 物件可以序列或併發地清除(但要保持 GIL);無論哪種方式,有些會先完成。已終結物件必須能夠容忍其引用物件子集的清除。PEP 442 將此階段稱為“迴圈垃圾”。
洩漏: 如果一個迴圈隔離體在組中所有物件都被終結和清除後仍然存在,那麼這些物件將無限期地無法回收(參見
gc.garbage
)。如果一個迴圈隔離體達到此階段,則說明存在一個錯誤——這意味著參與物件的tp_clear
方法未能按要求打破引用迴圈。
如果tp_clear
不存在,那麼 Python 將無法安全地打破引用迴圈。簡單地銷燬迴圈隔離體中的一個物件將導致懸空指標,當引用已銷燬物件的另一個物件本身被銷燬時,將觸發未定義行為。清除步驟使物件銷燬成為一個兩階段過程:首先呼叫tp_clear
來部分銷燬物件,使其足以相互解開,然後呼叫tp_dealloc
來完成銷燬。
與清除不同,終結不是銷燬的一個階段。已終結物件仍然必須透過繼續履行其設計契約來正常行為。物件的終結器被允許執行任意 Python 程式碼,甚至可以透過新增引用來阻止即將發生的銷燬。終結器只與呼叫順序有關——如果它執行,它會在銷燬之前執行,銷燬從 tp_clear
(如果被呼叫)開始,並以 tp_dealloc
結束。
終結步驟對於安全回收迴圈隔離體中的物件並不是必需的,但它的存在使得設計在物件被清除時能夠以合理方式行為的型別更容易。清除物件可能必然會使其處於損壞的、部分銷燬的狀態——呼叫任何被清除物件的方法或訪問其任何屬性可能是不安全的。透過終結,只有已終結的物件才可能與已清除的物件互動;未終結的物件被保證只與未清除(但可能已終結)的物件互動。
總結可能的互動
未終結物件可能擁有指向或來自未終結物件和已終結物件的引用,但沒有指向或來自已清除物件的引用。
已終結物件可能擁有指向或來自未終結、已終結和已清除物件的引用。
已清除物件可能擁有指向或來自已終結和已清除物件的引用,但沒有指向或來自未終結物件的引用。
如果沒有任何引用迴圈,物件在其最後一個引用被刪除後可以簡單地銷燬;終結和清除步驟對於安全回收未使用的物件不是必需的。然而,無論物件是否參與迴圈隔離體,讓所有物件始終經歷相同的事件序列可以簡化型別設計,因此在銷燬之前自動呼叫tp_finalize
和tp_clear
仍然可能很有用。Python 目前只在需要銷燬迴圈隔離體時呼叫tp_finalize
和tp_clear
;這在未來版本中可能會改變。
函式¶
要分配和釋放記憶體,請參閱在堆上分配物件。
-
void PyObject_CallFinalizer(PyObject *op)¶
如
tp_finalize
中所述,終結物件。請呼叫此函式(或PyObject_CallFinalizerFromDealloc()
),而不是直接呼叫tp_finalize
,因為此函式可能會對tp_finalize
的多次呼叫進行去重。目前,只有在型別支援垃圾回收(即設定了Py_TPFLAGS_HAVE_GC
標誌)的情況下才進行去重;這在未來可能會改變。
-
int PyObject_CallFinalizerFromDealloc(PyObject *op)¶
與
PyObject_CallFinalizer()
相同,但旨在在物件的解構函式 (tp_dealloc
) 開始時呼叫。不能有任何對物件的引用。如果物件的終結器復活了物件,此函式返回 -1;不應再進行銷燬。否則,此函式返回 0,銷燬可以正常繼續。參見
tp_dealloc
的示例程式碼。