Python內存管理機制 Java內存管理與垃圾回收

俗話說,出來混遲早要還的,Python還有不少知識點沒有總結成博客,欠了太多,先還一部分吧html

1. Python對象的內存使用

內存管理是語言設計的一個重要方面。它是決定語言性能的重要因素。不管是C語言的手工管理,仍是Java的垃圾回收,都成爲語言最重要的特徵。java

Python認爲一切都是對象,在使用對象時須要進行內存管理,簡單說,使用對象時須要借用系統資源,爲對象分配內存;用完之後,一樣須要釋放借用的系統資源(防止內存泄露,當一個對象已經不須要再使用本該被回收時,另一個正在使用的對象持有它的引用從而致使它不能被回收,這致使本該被回收的對象不能被回收而停留在堆內存中,這就產生了內存泄漏。);對於python程序員來講,python的解釋器承擔了內存管理的複雜任務,因此python程序員刻意沒必要關心內存管理的問題;可是,瞭解一下Python的內存管理機制仍是頗有必要的;python

1)引用計數機制

Python採用引用計數機制對內存進行管理;程序員

python認爲一切都是對象,它們的核心就是一個結構體:PyObjectapp

 typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

PyObject是每一個對象必有的內容,其中ob_refcnt就是作爲引用計數。當一個對象有新的引用時,它的ob_refcnt就會增長,當引用它的對象被刪除,它的ob_refcnt就會減小函數

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增長計數
#define Py_DECREF(op) \ //減小計數
    if (--(op)->ob_refcnt != 0) \
        ; \
    else \
        __Py_Dealloc((PyObject *)(op))

當引用計數爲0時,該對象生命就結束了。post

以常見的賦值語句爲例:性能

a = 'hello'

當python解釋器執行到這條語句,首先會建立一個字符串對象'hello',而後將該對象的引用賦值給a;在python中,存在一個內部跟蹤變量,用於記錄全部使用中的對象各有多少個引用,這個變量稱爲「引用計數」;url

經過sys包中的getrefcount(),能夠查看某個對象的引用計數;spa

 1 >>> import sys
 2 >>>
 3 >>> a = 'hello'
 4 >>> sys.getrefcount('hello')
 5 3
 6 >>> sys.getrefcount(a)
 7 2
 8 >>>
 9 >>> b = 'hello'
10 >>> sys.getrefcount('hello')
11 4
12 >>> sys.getrefcount(a)
13 3
14 >>>
15 >>> c = b
16 >>> sys.getrefcount('hello')
17 5
18 >>> sys.getrefcount(a)
19 4
20 >>>

上例中第3-6行中。‘hello’的引用計數爲3,首先'hello'被建立時引用計數爲1,以後將引用賦值給了a,引用計數加1,變爲2,以後經過getrefcount()函數查看引用計數時,引用計數再加1,變爲3;

上句是錯的,由於:

n.
>>> import sys
>>> sys.getrefcount('winter')
3
>>>

不清楚爲何開始'winter'的引用計數就是3,往後要搞明白,能夠參考

Fun with Python's sys.getrefcount()

同理,a在賦值語句以後引用計數爲1,以後經過getrefcount()函數查看引用計數時,引用計數加1,變爲2;

那麼第9-13行和第15-19行都是說明了什麼狀況會形成引用計數的增長

2)引用計數增長的狀況

  • 對象被建立:x = 3.14
  • 另外的別名被建立:y = x
  • 被做爲參數傳遞給函數(新的本地引用):foobar(x)
  • 成爲容器對象的一個元素:myList = [123, x, 'xyz']

3)引用計數減小的狀況:

  • 一個本地引用離開了其做用範圍。如foobar()函數結束時
  • 對象的別名被顯式銷燬:del y
  • 對象的一個別名被賦值給其餘對象:x = 123
  • 對象被從一個窗口對象中移除:myList.remove(x)
  • 窗口對象自己被銷燬:del myList

其中del xxx會有兩個結果,舉例說明:

>>> import sys
>>>
>>> a = 'hello'
>>> b = a
>>>
>>> sys.getrefcount('hello')
4
>>> sys.getrefcount(a)
3
>>> sys.getrefcount(b)
3
>>>
>>> del a
>>>
>>> sys.getrefcount('hello')
3
>>> sys.getrefcount(b)
2
>>> sys.getrefcount(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>>

結果是:首先從目前的命名空間刪除了a,同時,b和'hello'的引用計數減1

2. 垃圾回收機制

吃太多,總會變胖,Python也是這樣。當Python中的對象愈來愈多,它們將佔據愈來愈大的內存。不過你不用太擔憂Python的體形,它會乖巧的在適當的時候「減肥」,啓動垃圾回收(garbage collection),將沒用的對象清除。在許多語言中都有垃圾回收機制,好比Java和Ruby。儘管最終目的都是塑造苗條的提醒,但不一樣語言的減肥方案有很大的差別 ,關於java,可參看Java內存管理與垃圾回收

 Python中的垃圾回收是以引用計數爲主,標記-清除和分代收集爲輔。

Python的GC模塊主要運用了「引用計數」(reference counting)來跟蹤和回收垃圾。在引用計數的基礎上,還能夠經過「標記-清除」(mark and sweep)解決容器對象可能產生的循環引用的問題。經過「分代回收」(generation collection)以空間換取時間來進一步提升垃圾回收的效率。

1)引用計數優缺點

當一個對象的引用被建立或者複製時,對象的引用計數加1;當一個對象的引用被銷燬時,對象的引用計數減1;當對象的引用計數減小爲0時,就意味着對象已經沒有被任何人使用了,能夠將其所佔用的內存釋放了。

優勢:

雖然引用計數必須在每次分配和釋放內存的時候加入管理引用計數的動做,然而與其餘主流的垃圾收集技術相比,引用計數有一個最大的優勢,即「實時性」,任何內存,一旦沒有指向它的引用,就會當即被回收。而其餘的垃圾收集計數必須在某種特殊條件下(好比內存分配失敗)才能進行無效內存的回收。

缺點:

引用計數機制所帶來的維護引用計數的額外操做與Python運行中所進行的內存分配和釋放,引用賦值的次數是成正比的。而這點相比其餘主流的垃圾回收機制,好比「標記-清除」,「中止-複製」,是一個弱點,由於這些技術所帶來的額外操做基本上只是與待回收的內存數量有關。

若是說執行效率還僅僅是引用計數機制的一個軟肋的話,那麼很不幸,引用計數機制還存在着一個致命的弱點,正是因爲這個弱點,使得俠義的垃圾收集歷來沒有將引用計數包含在內,能引起出這個致命的弱點就是循環引用(也稱交叉引用)

 2)循環引用

包含其餘對象引用的容器對象(好比:list,set,dict,class,instance)均可能產生循環引用。

首先,先看一個小例子:

 1 >>>
 2 >>> import sys
 3 >>>
 4 >>> a = []
 5 >>> b = []
 6 >>> a.append(b)
 7 >>>
 8 >>> id(b)
 9 41589704
10 >>>
11 >>> sys.getrefcount(a)
12 2
13 >>> sys.getrefcount(b)
14 3
15 >>> del b
16 >>>
17 >>>
18 >>> sys.getrefcount(a)
19 2
20 >>> id(a[0])
21 41589704
22 >>>

上例中咱們看到,在a.append(b)之後,b的引用計數變爲2(第14行,顯示爲3,其實是2,由於getrefcount增長了引用計數);在咱們del b之後,b的引用計數-1,變爲1(實際應該是b引用的[]的引用計數);爲了肯定咱們的理論正確,咱們經過比較id值來講明實際上b引用的[]還在佔據佔據內存空間(第8行和第21行);這是該對象是可達的,由於a中還在引用他;若是最後del a的話,b引用的[]的引用計數就會再減1,變爲0,就會被回收;

那麼,再看一個引用計數的小例子:

 1 >>> import sys
 2 >>>
 3 >>> a = []
 4 >>> b = []
 5 >>> a.append(b)
 6 >>> b.append(a)
 7 >>>
 8 >>> sys.getrefcount(a)
 9 3
10 >>> sys.getrefcount(b)
11 3
12 >>>
13 >>> del a
14 >>> del b
15 >>>

上面的例子中,在第6行之後,能夠看到a和b的引用計數都是2(忽略getrefcount增長的引用計數),那麼在del a和del b之後,a和b的引用計數爲1,非0;因此引用計數機制就沒法回收,形成了內存泄露;

 經過「標記-清除」的方法來解決循環引用問題:

3)標記-清除

「標記-清除」是爲了解決循環引用的問題。能夠包含其餘對象引用的容器對象(好比:list,set,dict,class,instance)均可能產生循環引用。
咱們必須認可一個事實,若是兩個對象的引用計數都爲1,可是僅僅存在他們之間的循環引用,那麼這兩個對象都是須要被回收的,也就是說,它們的引用計數雖然表現爲非0,但實際上有效的引用計數爲0。咱們必須先將循環引用摘掉,那麼這兩個對象的有效計數就現身了。假設兩個對象爲A、B,咱們從A出發,由於它有一個對B的引用,則將B的引用計數減1;而後順着引用達到B,由於B有一個對A的引用,一樣將A的引用減1,這樣,就完成了循環引用對象間環摘除。
可是這樣就有一個問題,假設對象A有一個對象引用C,而C沒有引用A,若是將C計數引用減1,而最後A並無被回收,顯然,咱們錯誤的將C的引用計數減1,這將致使在將來的某個時刻出現一個對C的懸空引用。這就要求咱們必須在A沒有被刪除的狀況下復原C的引用計數,若是採用這樣的方案,那麼維護引用計數的複雜度將成倍增長。

原理:「標記-清除」採用了更好的作法,咱們並不改動真實的引用計數,而是將集合中對象的引用計數複製一份副本,改動該對象引用的副本。對於副本作任何的改動,都不會影響到對象生命走起的維護。
這個計數副本的惟一做用是尋找root object集合(該集合中的對象是不能被回收的)。當成功尋找到root object集合以後,首先將如今的內存鏈表一分爲二,一條鏈表中維護root object集合,成爲root鏈表,而另一條鏈表中維護剩下的對象,成爲unreachable鏈表。之因此要剖成兩個鏈表,是基於這樣的一種考慮:如今的unreachable可能存在被root鏈表中的對象,直接或間接引用的對象,這些對象是不能被回收的,一旦在標記的過程當中,發現這樣的對象,就將其從unreachable鏈表中移到root鏈表中;當完成標記後,unreachable鏈表中剩下的全部對象就是名副其實的垃圾對象了,接下來的垃圾回收只需限制在unreachable鏈表中便可。

4)分代回收

背景:分代的垃圾收集技術是在上個世紀80年代初發展起來的一種垃圾收集機制,一系列的研究代表:不管使用何種語言開發,不管開發的是何種類型,何種規模的程序,都存在這樣一點相同之處。即:必定比例的內存塊的生存週期都比較短,一般是幾百萬條機器指令的時間,而剩下的內存塊,起生存週期比較長,甚至會從程序開始一直持續到程序結束。
從前面「標記-清除」這樣的垃圾收集機制來看,這種垃圾收集機制所帶來的額外操做實際上與系統中總的內存塊的數量是相關的,當須要回收的內存塊越多時,垃圾檢測帶來的額外操做就越多,而垃圾回收帶來的額外操做就越少;反之,當需回收的內存塊越少時,垃圾檢測就將比垃圾回收帶來更少的額外操做。爲了提升垃圾收集的效率,採用「空間換時間的策略」。

原理:將系統中的全部內存塊根據其存活時間劃分爲不一樣的集合,每個集合就成爲一個「代」,垃圾收集的頻率隨着「代」的存活時間的增大而減少。也就是說,活得越長的對象,就越不多是垃圾,就應該減小對它的垃圾收集頻率。那麼如何來衡量這個存活時間:一般是利用幾回垃圾收集動做來衡量,若是一個對象通過的垃圾收集次數越多,能夠得出:該對象存活時間就越長。

舉例說明:

當某些內存塊M通過了3次垃圾收集的清洗以後還存活時,咱們就將內存塊M劃到一個集合A中去,而新分配的內存都劃分到集合B中去。當垃圾收集開始工做時,大多數狀況都只對集合B進行垃圾回收,而對集合A進行垃圾回收要隔至關長一段時間後才進行,這就使得垃圾收集機制須要處理的內存少了,效率天然就提升了。在這個過程當中,集合B中的某些內存塊因爲存活時間長而會被轉移到集合A中,固然,集合A中實際上也存在一些垃圾,這些垃圾的回收會由於這種分代的機制而被延遲。
在Python中,總共有3「代」,也就是Python實際上維護了3條鏈表。具體能夠查看Python源碼詳細瞭解。

在Python中,採用分代收集的方法。把對象分爲三代,一開始,對象在建立的時候,放在一代中,若是在一次一代的垃圾檢查中,改對象存活下來,就會被放到二代中,同理在一次二代的垃圾檢查中,該對象存活下來,就會被放到三代中。

gc模塊裏面會有一個長度爲3的列表的計數器,能夠經過gc.get_count()獲取。
例如(488,3,0),其中488是指距離上一次一代垃圾檢查,Python分配內存的數目減去釋放內存的數目,注意是內存分配,而不是引用計數的增長。例如:

3是指距離上一次二代垃圾檢查,一代垃圾檢查的次數,同理,0是指距離上一次三代垃圾檢查,二代垃圾檢查的次數。

gc模快有一個自動垃圾回收的閥值,即經過gc.get_threshold函數獲取到的長度爲3的元組,例如(700,10,10)
每一次計數器的增長,gc模塊就會檢查增長後的計數是否達到閥值的數目,若是是,就會執行對應的代數的垃圾檢查,而後重置計數器
例如,假設閥值是(700,10,10)

    • 當計數器從(699,3,0)增長到(700,3,0),gc模塊就會執行gc.collect(0),即檢查一代對象的垃圾,並重置計數器爲(0,4,0)
    • 當計數器從(699,9,0)增長到(700,9,0),gc模塊就會執行gc.collect(1),即檢查1、二代對象的垃圾,並重置計數器爲(0,0,1)
    • 當計數器從(699,9,9)增長到(700,9,9),gc模塊就會執行gc.collect(2),即檢查1、2、三代對象的垃圾,並重置計數器爲(0,0,0)

 

未完待續。。。擴展gc模塊

相關文章
相關標籤/搜索