Python的內存管理,一般從以下三個方面來說:
在網站制作、網站建設過程中,需要針對客戶的行業(yè)特點、產品特性、目標受眾和市場情況進行定位分析,以確定網站的風格、色彩、版式、交互等方面的設計方向。創(chuàng)新互聯(lián)還需要根據客戶的需求進行功能模塊的開發(fā)和設計,包括內容管理、前臺展示、用戶權限管理、數據統(tǒng)計和安全保護等功能。
1)對象的引用計數機制(四增五減)
2)垃圾回收機制(手動自動,分代回收)
3)內存池機制(大m小p)
1)對象的引用計數機制
要保持追蹤內存中的對象,Python使用了引用計數這一簡單的技術。sys.getrefcount(a)可以查看a對象的引用計數,但是比正常計數大1,因為調用函數的時候傳入a,這會讓a的引用計數+1
2)垃圾回收機制
吃太多,總會變胖,Python也是這樣。當Python中的對象越來越多,它們將占據越來越大的內存。不過你不用太擔心Python的體形,它會在適當的時候“減肥”,啟動垃圾回收(garbage
collection),將沒用的對象清除
從基本原理上,當Python的某個對象的引用計數降為0時,說明沒有任何引用指向該對象,該對象就成為要被回收的垃圾了
比如某個新建對象,它被分配給某個引用,對象的引用計數變?yōu)?。如果引用被刪除,對象的引用計數為0,那么該對象就可以被垃圾回收。
然而,減肥是個昂貴而費力的事情。垃圾回收時,Python不能進行其它的任務。頻繁的垃圾回收將大大降低Python的工作效率。如果內存中的對象不多,就沒有必要總啟動垃圾回收。
所以,Python只會在特定條件下,自動啟動垃圾回收。當Python運行時,會記錄其中分配對象(object
allocation)和取消分配對象(object deallocation)的次數。當兩者的差值高于某個閾值時,垃圾回收才會啟動。
我們可以通過gc模塊的get_threshold()方法,查看該閾值。
3)內存池機制
Python中有分為大內存和小內存:(256K為界限分大小內存)
1、大內存使用malloc進行分配
2、小內存使用內存池進行分配
python中的內存管理機制都有兩套實現,一套是針對小對象,就是大小小于256K時,pymalloc會在內存池中申請內存空間;當大于256K時,則會直接執(zhí)行系統(tǒng)的malloc的行為來申請內存空間。
眾所周知,Python 是一門面向對象語言,在 Python 的世界一切皆對象。所以一切變量的本質都是對象的一個指針而已。
Python 運行過程中會不停的創(chuàng)建各種變量,而這些變量是需要存儲在內存中的,隨著程序的不斷運行,變量數量越來越多,所占用的空間勢必越來越大,如果對變量所占用的內存空間管理不當的話,那么肯定會出現 out of memory。程序大概率會被異常終止。
因此,對于內存空間的有效合理管理變得尤為重要,那么 Python 是怎么解決這個問題的呢。其實很簡單,對不不可能再使用到的內存進行回收即可,像 C 語言中需要程序員手動釋放內存就是這個道理。但問題是如何確定哪些內存不再會被使用到呢?這就是我們今天要說的垃圾回收了。
目前垃圾回收比較通用的解決辦法有三種,引用計數,標記清除以及分代回收。
引用計數也是一種最直觀,最簡單的垃圾收集技術。在 Python 中,大多數對象的生命周期都是通過對象的引用計數來管理的。其原理非常簡單,我們?yōu)槊總€對象維護一個 ref 的字段用來記錄對象被引用的次數,每當對象被創(chuàng)建或者被引用時將該對象的引用次數加一,當對象的引用被銷毀時該對象的引用次數減一,當對象的引用次數減到零時說明程序中已經沒有任何對象持有該對象的引用,換言之就是在以后的程序運行中不會再次使用到該對象了,那么其所占用的空間也就可以被釋放了了。
我們來看看下面的例子。
函數 print_memory_info 用來獲取程序占用的內存空間大小,在 foo 函數中創(chuàng)建一個包含一百萬個整數的列表。從打印結果我們可以看出,創(chuàng)建完列表之后程序耗用的內存空間上升到了 55 MB。而當函數 foo 調用完畢之后內存消耗又恢復正常。
這是因為我們在函數 foo 中創(chuàng)建的 list 變量是局部變量,其作用域是當前函數內部,一旦函數執(zhí)行完畢,局部變量的引用會被自動銷毀,即其引用次數會變?yōu)榱悖加玫膬却婵臻g也會被回收。
為了驗證我們的想法,我們對函數 foo 稍加改造。代碼如下:
稍加改造之后,即使 foo 函數調用結束其所消耗的內存也未被釋放。
主要是因為我們將函數 foo 內部產生的列表返回并在主程序中接收之后,這樣就會導致該列表的引用依然存在,該對象后續(xù)仍有可能被使用到,垃圾回收便不會回收該對象。
那么,什么時候對象的引用次數才會增加呢。下面四種情況都會導致對象引用次數加一。
同理,對象引用次數減一的情況也有四種。
引用計數看起來非常簡單,實現起來也不復雜,只需要維護一個字段保存對象被引用的次數即可,那么是不是就代表這種算法沒有缺點了呢。實則不然,我們知道引用次數為零的對象所占用的內存空間肯定是需要被回收的。那引用次數不為零的對象呢,是不是就一定不能回收呢?
我們來看看下面的例子,只是對函數 foo 進行了改造,其余未做更改。
我們看到,在函數 foo 內部生成了兩個列表 list_a 和 list_b,然后將兩個列表分別添加到另外一個中。由結果可以看出,即使 foo 函數結束之后其所占用的內存空間依然未被釋放。這是因為對于 list_a 和 list_b 來說雖然沒有被任何外部對象引用,但因為二者之間交叉引用,以至于每個對象的引用計數都不為零,這也就造成了其所占用的空間永遠不會被回收的尷尬局面。這個缺點是致命的。
為了解決交叉引用的問題,Python 引入了標記清除算法和分代回收算法。
顯然,可以包含其他對象引用的容器對象都有可能產生交叉引用問題,而標記清除算法就是為了解決交叉引用的問題的。
標記清除算法是一種基于對象可達性分析的回收算法,該算法分為兩個步驟,分別是標記和清除。標記階段,將所有活動對象進行標記,清除階段將所有未進行標記的對象進行回收即可。那么現在的為問題變?yōu)榱?GC 是如何判定哪些是活動對象的?
事實上 GC 會從根結點出發(fā),與根結點直接相連或者間接相連的對象我們將其標記為活動對象(該對象可達),之后進行回收階段,將未標記的對象(不可達對象)進行清除。前面所說的根結點可以是全局變量,也可以是調用棧。
標記清除算法主要用來處理一些容器對象,雖說該方法完全可以做到不誤殺不遺漏,但 GC 時必須掃描整個堆內存,即使只有少量的非可達對象需要回收也需要掃描全部對象。這是一種巨大的性能浪費。
由于標記清除算法需要掃描整個堆的所有對象導致其性能有所損耗,而且當可以回收的對象越少時性能損耗越高。因此 Python 引入了分代回收算法,將系統(tǒng)中存活時間不同的對象劃分到不同的內存區(qū)域,共三代,分別是 0 代,1 代 和 2 代。新生成的對象是 0 代,經過一次垃圾回收之后,還存活的對象將會升級到 1 代,以此類推,2 代中的對象是存活最久的對象。
那么什么時候觸發(fā)進行垃圾回收算法呢。事實上隨著程序的運行會不斷的創(chuàng)建新的對象,同時也會因為引用計數為零而銷毀大部分對象,Python 會保持對這些對象的跟蹤,由于交叉引用的存在,以及程序中使用了長時間存活的對象,這就造成了新生成的對象的數量會大于被回收的對象數量,一旦二者之間的差值達到某個閾值就會啟動垃圾回收機制,使用標記清除算法將死亡對象進行清除,同時將存活對象移動到 1 代。 以此類推,當二者的差值再次達到閾值時又觸發(fā)垃圾回收機制,將存活對象移動到 2 代。
這樣通過對不同代的閾值做不同的設置,就可以做到在不同代使用不同的時間間隔進行垃圾回收,以追求性能最大。
事實上,所有的程序都有一個相識的現象,那就是大部分的對象生存周期都是相當短的,只有少量對象生命周期比較長,甚至會常駐內存,從程序開始運行持續(xù)到程序結束。而通過分代回收算法,做到了針對不同的區(qū)域采取不同的回收頻率,節(jié)約了大量的計算從而提高 Python 的性能。
除了上面所說的差值達到一定閾值會觸發(fā)垃圾回收之外,我們還可以顯示的調用 gc.collect() 來觸發(fā)垃圾回收,最后當程序退出時也會進行垃圾回收。
本文介紹了 Python 的垃圾回收機制,垃圾回收是 Python 自帶的功能,并不需要程序員去手動管理內存。
其中引用計數法是最簡單直接的,但是需要維護一個字段且針對交叉引用無能為力。
標記清除算法主要是為了解決引用計數的交叉引用問題,該算法的缺點就是需要掃描整個堆的所有對象,有點浪費性能。
而分代回收算法的引入則完美解決了標記清除算法需要掃描整個堆對象的性能浪費問題。該算法也是建立在標記清除基礎之上的。
最后我們可以通過 gc.collect() 手動觸發(fā) GC 的操作。
題外話,如果你看過 JVM 的垃圾回收算法之后會發(fā)現 Python 的垃圾回收算法與其是如出一轍的,事實再次證明,程序語言設計時是會相互參考的。
會
問題解決,只要沒有循環(huán)套用返回值本身,將其賦值給新變量,然后del原變量,gc.collect()之后,內存即可回收
1.Python的垃圾回收機制原理
Python無需我們手動回收內存,它的垃圾回收是如何實現的呢?
引用計數為主(缺點:循環(huán)引用無法解決)
引入標記清除和分代回收解決引用計數問題
引用計數為主+標記清除和分代回收為輔
垃圾回收(GC)
(1)引用計數
a = [1] # [1]對象引用計數增加1,ref=1
b = a # [1]對象引用計數增加1,ref=2
b = None # [1]對象引用計數減少1,ref=1
del a # [1]對象引用計數減少1,ref=0
a = [1],當把列表 [1] 賦值給 a 的時候,它的引用計數就會增加1,此時列表 [1] 對象的引用計數ref=1 ; b = a 又把 a 賦值給 b ,a和b 同時引用了列表[1]對象,ref又增加1,此時 ref =2。繼續(xù)執(zhí)行 b = None, 讓b指向None,這個時候它就不會指向原來的列表[1]對象,列表[1]對象的引入計數就會減少1,又變成ref=1。執(zhí)行del a ,引用計數就會減少1,這個時候 ref = 0。當對象的引用計數為0就可以回收掉,
注意:del 作用就會減少對象引用計數,并不是銷毀對象。只有當引用計數為0的時候,Python解釋器才回去把對象占用的內存回收掉。
// object.h
struct _object {
Py_ssize_t ob_refcnt; # 引用計數值
}PyObject;
① 什么時候引用計數增加呢?
② 什么時候引用計數會減少呢?
(2)引用計數無法解決循環(huán)引用問題
循環(huán)引用
a = [1] # 對象[1]引用計數增加1,ref=1
b = [2] # 對象[2]引用計數增加1,ref=1
a.append(b) # b被a引用,對象[2]引用計數增加1,ref=2
b.append(a) # a又被b引用,對象[1]引用計數增加1,ref=2
del a # 對象[1]引用計數減少1,ref=1
del b # 對象[2]引用計數減少1,ref=1
(3)標記清除(Mark and Sweep)
(4)分代回收
import gc