2019python面試題-垃圾回收(GC)機制


一、什么是GC

在Java中,對象所占用的內存在對象不再使用后會自動被回收。這些工作是由一個叫垃圾回收器 (Garbage Collector )的進程完成的。

python和其他很多高級語言一樣,都自帶垃圾回收機制,即GC機制。

 

二、GC機制

 Python中的垃圾回收是以引用計數為主,標記-清除和分代收集為輔。引用計數最大缺陷就是循環引用的問題,所以Python采用了輔助方法。

 

注意:

  1、垃圾回收時,Python不能進行其它的任務,頻繁的垃圾回收將大大降低Python的工作效率;

  2、Python只會在特定條件下,自動啟動垃圾回收(垃圾對象少就沒必要回收)

  3、當Python運行時,會記錄其中分配對象(object allocation)和取消分配對象(object deallocation)的次數。當兩者的差值高於某個閾值時,垃圾回收才會啟動。

1、引用計數

Python內部使用引用計數,來保持追蹤內存中的對象,所有對象都有引用計數。

原理

引用計數法的原理是每個對象維護一個ob_refcnt,用來記錄當前對象被引用的次數,也就是來追蹤到底有多少引用指向了這個對象。當一個對象有新的引用時,它的ob_refcnt就會增加,當引用它的對象被刪除,它的ob_refcnt就會減少。當引用計數為0時,該對象生命就結束了。

源碼如下:

// object.h
struct _object {
    Py_ssize_t ob_refcnt;  # 引用計數值
    struct PyTypeObject *ob_type;
} PyObject;

 

引用計數增加的情況:

  1. 對象被創建:x='spam'
  2. 用另一個別名被創建:y=x
  3. 被作為參數傳遞給函數:foo(x)
  4. 作為容器對象的一個元素:a=[1,x,'33']

引用計數減少情況

  1. 一個本地引用離開了它的作用域。比如上面的foo(x)函數結束時,x指向的對象引用減1。
  2. 對象的別名被顯式的銷毀:del x ;或者del y
  3. 對象的一個別名被賦值給其他對象:x=789
  4. 對象從一個窗口對象中移除:myList.remove(x)
  5. 窗口對象本身被銷毀:del myList,或者窗口對象本身離開了作用域。

引用計數法有很明顯的優點:

  1. 高效
  2. 運行期沒有停頓 可以類比一下Ruby的垃圾回收機制,也就是 實時性:一旦沒有引用,內存就直接釋放了。不用像其他機制等到特定時機。實時性還帶來一個好處:處理回收內存的時間分攤到了平時。 對象有確定的生命周期
  3. 易於實現

原始的引用計數法也有明顯的缺點:

  1. 維護引用計數消耗資源,維護引用計數的次數和引用賦值成正比,而不像mark and sweep等基本與回收的內存數量有關。
  2. 無法解決循環引用的問題。A和B相互引用而再沒有外部引用A與B中的任何一個,它們的引用計數都為1,但顯然應該被回收。
# 循環引用的示例:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)

對於現如今強大的硬件來說,缺點維護引用計數消耗資源還尚可接受,但是循環引用導致內存泄漏卻無疑是致命的,因此python還引入了新的回收機制(標記清除和分代收集)

 

2、標記-清除算法

針對循環引用的情況:我們有一個“孤島”或是一組未使用的、互相指向的對象,但是誰都沒有外部引用。換句話說,我們的程序不再使用這些節點對象了,所以我們希望Python的垃圾回收機制能夠足夠智能去釋放這些對象並回收它們占用的內存空間。但是這不可能,因為所有的引用計數都是1而不是0。Python的引用計數算法不能夠處理互相指向自己的對象。你的代碼也許會在不經意間包含循環引用並且你並未意識到。事實上,當你的Python程序運行的時候它將會建立一定數量的“浮點數垃圾”,Python的GC不能夠處理未使用的對象因為應用計數值不會到零。
 這就是為什么Python要引入Generational GC算法的原因! 

『標記清除(Mark—Sweep)』算法是一種基於追蹤回收(tracing GC)技術實現的垃圾回收算法。它分為兩個階段:第一階段是標記階段,GC會把所有的『活動對象』打上標記,第二階段是把那些沒有標記的對象『非活動對象』進行回收。那么GC又是如何判斷哪些是活動對象哪些是非活動對象的呢?

對象之間通過引用(指針)連在一起,構成一個有向圖,對象構成這個有向圖的節點,而引用關系構成這個有向圖的邊。從根對象(root object)出發,沿着有向邊遍歷對象,可達的(reachable)對象標記為活動對象,不可達的對象就是要被清除的非活動對象。根對象就是全局變量、調用棧、寄存器。

標記清除算法作為Python的輔助垃圾收集技術主要處理的是一些容器對象,比如list、dict、tuple,instance等,因為對於字符串、數值對象是不可能造成循環引用問題。Python使用一個雙向鏈表將這些容器對象組織起來。不過,這種簡單粗暴的標記清除算法也有明顯的缺點:清除非活動的對象前它必須順序掃描整個堆內存,哪怕只剩下小部分活動對象也要掃描所有對象。

標記清除算法在申請內存時,所有容器對象的頭部又加上了PyGC_Head來實現“標記-清除”機制。任何一個python對象都分為兩部分: PyObject_HEAD + 對象本身數據

源碼如下:

// objimpl.h
typedef union _gc_head {
    struct {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy;  /* force worst-case alignment */
} PyGC_Head;

 

 

3、分代回收算法

  • Python將所有的對象分為0,1,2三代;
  • 所有的新建對象都是0代對象;
  • 當某一代對象經歷過垃圾回收,依然存活,就被歸入下一代對象。

python在創建對象之前,會創建一個鏈表,零代鏈表,只不過這個鏈表是空的。每當你創建一個對象,python便會將其加入到零代鏈表。

 

python隔代回收的核心:對鏈子上的那些明明沒有被引用但引用計數卻不是零的對象進行引用計數減去一,看看你是不是垃圾。如果被引用多次減去一之后仍不為零,那么會在零代鏈表當中繼續被清理,直至引用計數為零。因為如果沒有變量指向它,或者作為函數的參數,列表的元素等等,那么它就始終是零代鏈表中被清理的對象。當零代鏈表被清理達到一定次數時,會清理一代鏈表。一代鏈表被清理達到一定次數時,會清理二代鏈表。

因此清理的頻率最高的是零代鏈表,其次是一代鏈表,再是二代鏈表。

 

注:python內存管理機制

原理:

  1. Python提供了對內存的垃圾收集機制,但是它將不用的內存放到內存池而不是返回給操作系統;
  2. Pymalloc機制:為了加速Python的執行效率,Python引入了一個內存池機制,用於管理對小塊內存的申請和釋放;
  3. 對於Python對象,如整數,浮點數和List,都有其獨立的私有內存池,對象間不共享他們的內存池。也就是說如果你分配又釋放了大量的整數,用於緩存這些整數的內存就不能再分配給浮點數。

 

內存池(金字塔):

  • 第3層:最上層,用戶對Python對象的直接操作
  • 第1層和第2層:內存池,有Python的接口函數PyMem_Malloc實現-----若請求分配的內存在1~256字節之間就使用內存池管理系統進行分配,調用malloc函數分配內存,但是每次只會分配一塊大小為256K的大塊內存,不會調用free函數釋放內存,將該內存塊留在內存池中以便下次使用。
  • 第0層:大內存-----若請求分配的內存大於256K,malloc函數分配內存,free函數釋放內存。
  • 第-1,-2層:操作系統進行操作


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM