物件生命週期

本節解釋了物件在其整個生命週期中,一個型別的槽位(slots)之間如何相互關聯。它並非旨在作為槽位的完整規範參考;相反,有關特定槽位的詳細資訊,請參閱型別物件結構中的槽位特定文件。

生命事件

下圖展示了一個物件在其生命週期中可能發生的事件順序。從 AB 的箭頭表示事件 B 可以在事件 A 發生後發生,箭頭的標籤表示 A 發生後 B 發生的條件。

Life Events - Python 文件 tp_new - Python 文件 tp_new start->tp_new - Python 文件    type call   tp_alloc - Python 文件 tp_alloc tp_new->tp_alloc - Python 文件  direct call   tp_init - Python 文件 tp_init tp_new->tp_init - Python 文件 reachable - Python 文件 reachable tp_init->reachable - Python 文件 reachable->tp_init - Python 文件 tp_traverse - Python 文件 tp_traverse reachable->tp_traverse - Python 文件  not in a    cyclic    isolate   reachable->tp_traverse - Python 文件  periodic    cyclic isolate     detection   finalized? - Python 文件 marked as finalized? reachable->finalized? - Python 文件  no refs   tp_finalize - Python 文件 tp_finalize reachable->tp_finalize - Python 文件  resurrected    (maybe remove    finalized mark)   uncollectable - Python 文件 uncollectable (leaked) reachable->uncollectable - Python 文件  cyclic    isolate    (no GC    support)   tp_dealloc - Python 文件 tp_dealloc reachable->tp_dealloc - Python 文件  no refs tp_traverse->finalized? - Python 文件  cyclic    isolate   finalized?->tp_finalize - Python 文件  no (mark    as finalized)   tp_clear - Python 文件 tp_clear finalized?->tp_clear - Python 文件  yes   tp_finalize->tp_clear - Python 文件  no refs or     cyclic isolate   tp_finalize->tp_dealloc - Python 文件  recommended  call (see  explanation) tp_finalize->tp_dealloc - Python 文件   no refs   tp_clear->uncollectable - Python 文件  cyclic    isolate   tp_clear->tp_dealloc - Python 文件  no refs   tp_free - Python 文件 tp_free tp_dealloc->tp_free - Python 文件    direct call  

說明

  • 透過呼叫物件的型別構造新物件時

    1. tp_new 被呼叫以建立一個新物件。

    2. tp_new 直接呼叫 tp_alloc 來為新物件分配記憶體。

    3. tp_init 初始化新建立的物件。如果需要,可以再次呼叫 tp_init 來重新初始化物件。tp_init 呼叫也可以完全跳過,例如透過 Python 程式碼呼叫 __new__()

  • tp_init 完成後,物件即可使用。

  • 在最後一個對物件的引用被移除一段時間後

    1. 如果一個物件沒有被標記為 finalized,它可能會透過標記為 finalized 並呼叫其 tp_finalize 函式來終結。Python 會在對物件的最後一個引用被刪除時終結物件;請使用 PyObject_CallFinalizerFromDealloc() 來確保 tp_finalize 總是被呼叫。

    2. 如果物件被標記為已終結,垃圾回收器可能會呼叫 tp_clear 來清除物件持有的引用。當物件的引用計數達到零時,它會被呼叫。

    3. tp_dealloc 被呼叫以銷燬物件。為了避免程式碼重複,tp_dealloc 通常會呼叫 tp_clear 來釋放物件的引用。

    4. tp_dealloc 完成物件銷燬後,它直接呼叫 tp_free(通常根據型別自動設定為 PyObject_Free()PyObject_GC_Del())來釋放記憶體。

  • 如果需要,tp_finalize 函式允許向物件新增引用。如果它這樣做了,物件就會被 復活,從而阻止其即將到來的銷燬。(只有 tp_finalize 允許復活物件;tp_cleartp_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)。

  1. 可達(尚未成為迴圈隔離體):所有物件都處於其正常的、可達狀態。可能存在引用迴圈,但外部引用意味著物件尚未被隔離。

  2. 不可達但一致: 對一組迴圈物件外部的最後一個引用已被移除,導致這些物件變得隔離(因此誕生了一個迴圈隔離體)。該組中的任何物件都尚未被終結或清除。迴圈隔離體將保持此階段,直到垃圾回收器的未來某個執行(不一定是下一次執行,因為下一次執行可能不會掃描每個物件)。

  3. 已終結和未終結混合: 迴圈隔離體中的物件是逐個終結的,這意味著在一段時間內,迴圈隔離體由已終結和未終結物件混合組成。終結順序未指定,因此它可能看起來是隨機的。已終結物件在未終結物件與其互動時必須以合理的方式行為,並且未終結物件必須能夠容忍其引用物件中任意子集的終結。

  4. 全部終結: 迴圈隔離體中的所有物件都在其中任何一個被清除之前被終結。

  5. 已終結和已清除混合: 物件可以序列或併發地清除(但要保持 GIL);無論哪種方式,有些會先完成。已終結物件必須能夠容忍其引用物件子集的清除。PEP 442 將此階段稱為“迴圈垃圾”。

  6. 洩漏: 如果一個迴圈隔離體在組中所有物件都被終結和清除後仍然存在,那麼這些物件將無限期地無法回收(參見gc.garbage)。如果一個迴圈隔離體達到此階段,則說明存在一個錯誤——這意味著參與物件的tp_clear方法未能按要求打破引用迴圈。

如果tp_clear不存在,那麼 Python 將無法安全地打破引用迴圈。簡單地銷燬迴圈隔離體中的一個物件將導致懸空指標,當引用已銷燬物件的另一個物件本身被銷燬時,將觸發未定義行為。清除步驟使物件銷燬成為一個兩階段過程:首先呼叫tp_clear來部分銷燬物件,使其足以相互解開,然後呼叫tp_dealloc來完成銷燬。

與清除不同,終結不是銷燬的一個階段。已終結物件仍然必須透過繼續履行其設計契約來正常行為。物件的終結器被允許執行任意 Python 程式碼,甚至可以透過新增引用來阻止即將發生的銷燬。終結器只與呼叫順序有關——如果它執行,它會在銷燬之前執行,銷燬從 tp_clear(如果被呼叫)開始,並以 tp_dealloc 結束。

終結步驟對於安全回收迴圈隔離體中的物件並不是必需的,但它的存在使得設計在物件被清除時能夠以合理方式行為的型別更容易。清除物件可能必然會使其處於損壞的、部分銷燬的狀態——呼叫任何被清除物件的方法或訪問其任何屬性可能是不安全的。透過終結,只有已終結的物件才可能與已清除的物件互動;未終結的物件被保證只與未清除(但可能已終結)的物件互動。

總結可能的互動

  • 未終結物件可能擁有指向或來自未終結物件和已終結物件的引用,但沒有指向或來自已清除物件的引用。

  • 已終結物件可能擁有指向或來自未終結、已終結和已清除物件的引用。

  • 已清除物件可能擁有指向或來自已終結和已清除物件的引用,但沒有指向或來自未終結物件的引用。

如果沒有任何引用迴圈,物件在其最後一個引用被刪除後可以簡單地銷燬;終結和清除步驟對於安全回收未使用的物件不是必需的。然而,無論物件是否參與迴圈隔離體,讓所有物件始終經歷相同的事件序列可以簡化型別設計,因此在銷燬之前自動呼叫tp_finalizetp_clear仍然可能很有用。Python 目前只在需要銷燬迴圈隔離體時呼叫tp_finalizetp_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 的示例程式碼。