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

1、什麼是GC

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

python和其餘不少高級語言同樣,都自帶垃圾回收機制,即GC機制。算法

 

2、GC機制

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

 

注意:app

  一、垃圾回收時,Python不能進行其它的任務,頻繁的垃圾回收將大大下降Python的工做效率;函數

  二、Python只會在特定條件下,自動啓動垃圾回收(垃圾對象少就不必回收)spa

  三、當Python運行時,會記錄其中分配對象(object allocation)和取消分配對象(object deallocation)的次數。當二者的差值高於某個閾值時,垃圾回收纔會啓動。操作系統

一、引用計數

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

原理code

引用計數法的原理是每一個對象維護一個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還引入了新的回收機制(標記清除和分代收集)

 

二、標記-清除算法

針對循環引用的狀況:咱們有一個「孤島」或是一組未使用的、互相指向的對象,可是誰都沒有外部引用。換句話說,咱們的程序再也不使用這些節點對象了,因此咱們但願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;

 

 

三、分代回收算法

  • 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層:操做系統進行操做
相關文章
相關標籤/搜索