深入理解Python內存管理與垃圾回收

      網友投稿 924 2025-04-02

      面試官:聽說你學Python?那你給我講講Python如何進行內存管理?


      我:???內存管理不太清楚額。。。

      面試官:那你知道Python垃圾回收嗎?

      我:(尷尬一下后,還好我看到過相關博客)Python垃圾回收引用計數為主、標記清除和分代回收為主。

      面試官:那你仔細講講這三種垃圾回收技術?

      我:卒。。。

      先看看內存管理

      內存的管理簡單來說:分配(malloc)+回收(free)。

      再我們看文章之前,先思考一下:如果是你設計,會怎么進行內存管理?答:好,不會設計(筆主也不會),會的大佬請繞過。我們一起了解看看Python是怎么設計的。為了提高效率就是:

      深入理解Python內存管理與垃圾回收

      如何高效分配?

      如何有效回收?

      什么是內存

      買電腦的配置“4G + 500G / 1T”,這里的4G就是指電腦的內存容量,而電腦的硬盤 500G / 1T。

      內存(Memory,全名指內部存儲器),自然就會想到外存,他們都硬件設備。

      內存是計算機中重要的部件之一,它是外存與CPU進行溝通的橋梁。計算機中所有程序的運行都是在內存中進行的,因此內存的性能對計算機的影響非常大。

      內存就像一本空白的書

      由于不允許彼此書寫,因此必須注意他們能書寫的頁面。開始書寫之前,請先咨詢書籍管理員。然后,管理員決定允許他們在書中寫什么。

      如果這書已經存在很長時間了,因此其中的許多故事都不再適用。當沒有人閱讀或引用故事時,它們將被刪除以為新故事騰出空間。

      本質上,計算機內存就像一本空書。實際上,調用固定長度的連續內存頁面塊是很常見的,因此這種類比非常適用。

      以上類比出自此文

      內存管理:從硬件到軟件

      為什么4G內存的電腦可以高效的分析上G的數據,而且程序可以一直跑下去。

      在這4G內存的背后,Python都幫助我們做了什么?

      內存管理是應用程序讀取和寫入數據的過程。內存管理器確定將應用程序數據放置在何處。

      由于內存有限,類比書中的頁面一樣,管理員必須找到一些可用空間并將其提供給應用程序。提供內存的過程通常稱為內存分配。

      其實如果我們了解內存管理機制,以更快、更好的方式解決問題。

      看完本篇文章,帶您稍微了解Python內存管理的設計哲學。

      對象管理

      可能我們聽過,Python鼎鼎有名的那句“一切皆對象”。是的,在Python中數字是對象,字符串是對象,任何事物都是對象,Cpython下,而Python對象實現的核心就是一個結構體--PyObject。

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

      PyObject是每個對象必有的內容,可以說是Python中所有對象的祖父,僅包含兩件事:

      ob_refcnt:引用計數(reference count)

      ob_type:指向另一種類型的指針(pointer to another type)

      所以,所以CPython是用C編寫的,它解釋了Python字節碼。這與內存管理有什么關系?

      好吧,C中的CPython代碼中存在內存管理算法和結構。要了解Python的內存管理,您必須對CPython本身有一個基本的了解。其他我們也不深究,感興趣的同學自行了解。

      CPython的內存管理

      注:這一塊內容在網上找了很多內容,看了好久也沒懂,自己太菜。唯一看懂的就是 Alexander VanTol的文章相關部分內容,搬運過來哦放在此處,有刪減,有興趣的同學建議看原文。

      下圖的深灰色框現在歸Python進程所有。

      Python將部分內存用于內部使用和非對象內存。另一部分專用于對象存儲(您的int,dict等)。請注意,這已被簡化。如果您需要全貌,則可以看CPython源代碼,所有這些內存管理都在其中進行。

      CPython有一個對象分配器,負責在對象內存區域內分配內存。這個對象分配器是大多數魔術發生的地方。每當新對象需要分配或刪除空間時,都會調用該方法。

      通常,為list和int等Python對象添加和刪除數據一次不會涉及太多數據。因此,分配器的設計已調整為可以一次處理少量數據。它還嘗試在絕對需要之前不分配內存。

      現在,我們來看一下CPython的內存分配策略。首先,我們將討論這三個主要部分以及它們之間的關系。

      Python的內存分配器

      內存結構

      在Python中,當要分配內存空間時,不單純使用 malloc/free,而是在其基礎上堆放3個獨立的分層,有效率地進行分配。

      第 0 層往下是 OS 的功能。第 -2 層是隱含和機器的物理性相關聯的部分,OS 的虛擬內 存管理器負責這部分功能。第 -1 層是與機器實際進行交互的部分,OS 會執行這部分功能。 因為這部分的知識已經超出了本書的范圍,我們就不額外加以說明了。在第 3 層到第 0 層調用了一些具有代表性的函數,其調用圖如下。

      第0層 通用的基礎分配器

      以 Linux 為例,第 0 層指的就是 glibc 的 malloc() 這樣的分配器,是對 Linux 等 OS 申 請內存的部分。

      Python 中并不是在生成所有對象時都調用 malloc(),而是根據要分配的內存大小來改 變分配的方法。申請的內存大小如果大于 256 字節,就老實地調用 malloc();如果小于等 于 256 字節,就要輪到第 1 層和第 2 層出場了。

      更細致的過程:垃圾回收機制的算法與實現

      第1層 Python低級內存分配器

      Python 中使用的對象基本上都小于等于 256 字節,并且凈是一些馬上就會被廢棄的對象。請看下面的例子。

      for x in range(100): print(x)

      上述 Python 腳本是把從 0 到 99 的非負整數 A 轉化成字符串并輸出的程序。這個程序會大量使用一次性的小字符串。

      在這種情況下,如果逐次查詢第 0 層的分配器,就會發生頻繁調用 malloc() 和 free() 的情況,這樣一來效率就會降低。

      因此,在分配非常小的對象時,Python 內部會采用特殊的處理。實際執行這項處理的就是第 1 層和第 2 層的內存分配器。

      當需要分配小于等于 256 字節的對象時,就利用第 1 層的內存分配器。在這一層會事先 從第 0 層開始迅速保留內存空間,將其蓄積起來。第 1 層的作用就是管理這部分蓄積的空間。

      第1層處理的信息的內存結構

      根據所管理的內存空間的作用和大小的不同,我們稱最小 的單位為 block,最終返回給申請者的就是這個 block 的地址。比 block 大的單位的是 pool, pool 內部包含 block。pool 再往上叫作 arena。

      也就是說 arena > pool > block,感覺很像俄羅斯套娃吧。為了避免頻繁調用 malloc() 和 free(),第 0 層的分配器會以最大的單位 arena 來保留 內存。pool 是用于有效管理空的 block 的單位。arena 這個詞有“競技場”的意思。大家可以理解成競技場里有很多個 pool,pool 里面漂 浮著很多個 block,這樣或許更容易理解一些。

      Arenas是最大的內存塊,并在內存中的頁面邊界上對齊。頁面邊界是操作系統使用的固定長度連續內存塊的邊緣。Python假設系統的頁面大小為256 KB。

      Arenas內有內存池,池是一個虛擬內存頁(4 KB)。這些就像我們書中類比的頁面。這些池被分成較小的內存塊。

      給定池中的所有塊均具有相同的“大小等級”。給定一定數量的請求數據,大小類定義特定的塊大小。下圖直接取自源代碼注釋:

      這一點可以看Pymalloc

      針對小對象(<= 512 bytes),Pymalloc會在內存池中申請內存空間

      > 512bytes,則會PyMem_RawMalloc()和PyMem_RawRealloc()來申請新的內存空間

      例如,如果請求42個字節,則將數據放入48字節大小的塊中。

      arena 內部各個 pool 的大小固定在 4K 字節。因為幾乎對所有 OS 而言,其虛擬內存的頁 面大小都是 4K 字節,所以我們也相應地把 pool 的大小設定為 4K 字節。

      第1層總結

      第 1 層的任務可以用一句話來總結,那就是“管理 arena”。

      第2層 Python對象分配器

      第 2 層的分配器負責管理 pool 內的 block。這一層實際上是將 block 的開頭地址返回給申請者,并釋放 block 等。 那么我們來看看這一層是如何管理 block 的吧。

      pool 被分割成一個個的 block。我們在 Python 中生成對象時,最終都會被分配這個 block (在要求大小不大于 256 字節的情況下)。以 block 為單位來劃分,這是從 pool 初始化時就決定好的。這是因為我們一開始利用 pool 的時候就決定了“這是供 8 字節的 block 使用的 pool”。pool 內被 block 完全填滿了,那么 pool 是怎么進行 block 的狀態管理的呢?block 只有以下三種狀態。

      已經分配

      使用完畢

      未使用

      第3層 對象特有的分配器

      對象有列表和元組等多種多樣的型,在生成它們的時候要使用各自特有的分配器。

      賦值語句內存分析

      我們可以通過使用id()函數來查看某個對象的內存地址,每個人的電腦內存地址不一樣。

      a = 1 id(a) # Output: 4566652048 b = 2 id(b) # Output: 4566652080 c = 8 id(c) # Output: 4566652272 d = 8 id(d) # Output: 4566652272

      使用 ==來查看對象的值是否相等,is判斷對象是否是同一個對象

      c == d # Output: True c is d # Output: True e = 888888888 id(e) # Output: 4569828784 f = 888888888 id(f) # Output: 4569828880 e == f # Output: True e is f # Output: False

      解釋:我們可以看到,

      c == d輸出 True 和 c is d也輸出True,這是因為,對一個小一點的int變量賦值,Python在內存池(Pool)中分配給c和d同一塊內存地址,

      而e == f為 True,值相同;e is f輸出 False,并不少同一個對象。

      這是因為Python內存池中分配空間,賦予對象的類別并賦予其初始的值。從-5到256這些小的整數,在Python腳本中使用的非常頻繁,又因為他們是不可更改的,因此只創建一次,重復使用就可以了。

      e 和 f數字比較大,所以只能重新分配地址來。其實-5到256之間的數字,Python都已經給我安排好了。

      >>> i = 256 >>> j = 256 >>> i is j True >>> i = 257 >>> j = 257 >>> i is j False >>> i = -5 >>> j = -5 >>> i is j True >>> i = -6 >>> j = -6 >>> i is j False

      Java 也有這樣的機制 緩存范圍是 -128 ~ 127** Cache to support the object identity semantics of autoboxing for values between*** -128 and 127 (inclusive) as required by JLS.*

      接著,看對象的內存分析:

      li1 = [] li2 = [] li1 == li2 # Output: True li1 is li2 # Output: False x = 1 y = x id(x) # Output: 4566652048 id(y) # Output: 4566652048 y = 2 id(y) # Output: 4566652080 x == y # Output: False x is y # Output: False

      再來看看垃圾回收

      垃圾回收機制

      來看一下Python中的垃圾回收技術:

      引用計數為主

      標記清除和分代回收為輔

      如果一個對象的引用計數為0,Python解釋器就會回收這個對象的內存,但引用計數的缺點是不能解決循環引用的問題,所以我們需要標記清除和分代回收。

      什么是引用計數

      每個對象都有存有指向該對象的引用總數

      查看某個對象的引用計數sys.getrefcount()

      可以使用del關鍵字刪除某個引用

      import sys l = [] print(sys.getrefcount(l)) # Output: 2 l2 = l l3 = l l4 = l3 print(sys.getrefcount(l)) # Output: 5 del l2 print(sys.getrefcount(l)) # Output: 4 i = 1 print(sys.getrefcount(i)) # Output: 140 a = i print(sys.getrefcount(i)) # Output: 141

      當對象的引用計數達到零時,解釋器會暫停,來取消分配它以及僅可從該對象訪問的所有對象。即滿足引用計數為0的時候,會啟動垃圾回收。

      但是引用計數不能解決循環引用的問題,就如下的代碼不停跑就能把電腦內存跑滿:

      >>> a = [] >>> b = [] >>> while True: ... a.append(b) ... b.append(a) ... [1] 31962 killed python

      標記清除

      標記清除算法作為Python的輔助垃圾收集技術主要處理的是一些容器對象,比如list、dict、tuple,instance等,因為對于字符串、數值對象是不可能造成循環引用問題。標記清除和分代回收就是為了解決循環引用而生的。

      它分為兩個階段:第一階段是標記階段,GC會把所有的活動對象打上標記,第二階段是把那些沒有標記的對象非活動對象進行回收。

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

      在上圖中,可以從程序變量直接訪問塊1,并且可以間接訪問塊2和3。程序無法訪問塊4和5。第一步將標記塊1,并記住塊2和3以供稍后處理。第二步將標記塊2,第三步將標記塊3,但不記得塊2,因為它已被標記。掃描階段將忽略塊1,2和3,因為它們已被標記,但會回收塊4和5。

      標記清除算法作為Python的輔助垃圾收集技術,主要處理的是一些容器對象,比如list、dict、tuple等,因為對于字符串、數值對象是不可能造成循環引用問題。

      Python使用一個雙向鏈表將這些容器對象組織起來。不過,這種簡單粗暴的標記清除算法也有明顯的缺點:清除非活動的對象前它必須順序掃描整個堆內存,哪怕只剩下小部分活動對象也要掃描所有對象。

      分代回收(自動)

      分代回收是建立在標記清除技術基礎之上的,是一種以空間換時間的操作方式。

      Python將所有的對象分為0,1,2 三代

      所有的新建的對象都是0代對象

      當某一代對象經歷過垃圾回收,依然存活,那么它就被歸入下一代對象。

      同時,分代回收是建立在標記清除技術基礎之上。分代回收同樣作為Python的輔助垃圾收集技術處理那些容器對象。

      Python運行時,會記錄其中分配對象(object allocation)和取消分配對象(object deallocation)的次數。

      當兩者的差值高于某個閾值時,垃圾回收才會啟動

      查看閾值gc.get_threshold()

      import gc print(gc.get_threshold()) # Output: (700, 10, 10)

      get_threshold()返回的(700, 10, 10)返回的兩個10。也就是說,每10次0代垃圾回收,會配合1次1代的垃圾回收;而每10次1代的垃圾回收,才會有1次的2代垃圾回收。理論上,存活時間久的對象,使用的越多,越不容易被回收,這也是分代回收設計的思想。

      手動回收

      具體參考gc模塊。

      gc.collect()手動回收

      objgraph模塊中的count()記錄當前類產生的實例對象的個數

      import gc result = gc.collect() print(result)

      import objgraph

      class Person(Object): pass class Cat(object): pass p = Person() c = Cat() p.name ='yuzhou1su' c.master = p print(sys.getrefcount(p)) print(sys.getfefcount(c)) del p del c gc.collect() print(objgraph.count('Person')) print(objgraph.count('Cat'))

      當定位到哪個對象存在內存泄漏,就可以用show_backrefs查看這個對象的引用鏈。

      內存池(memory pool)機制

      頻繁 申請、消耗 會導致大量的內存碎片,致使效率變低。

      內存池的概念就是在內存中申請一定數量的,大小相等的內存塊留作備用。

      內存池池由單個大小類的塊組成。每個池維護一個到相同大小類的其他池的雙向鏈接列表。這樣,即使在不同的池中,該算法也可以輕松找到給定塊大小的可用空間。

      當有新的內存需求時,就會先從內存池中分配內存留給這個需求。內存不夠再申請新的內存。

      內存池本身必須處于以下三種狀態之一:

      已使用

      已滿

      或為空。

      優點:減少內存碎片,提高效率。

      總結

      內存管理是計算機的一個非常重要的組成部分。 Python 跟 Java、Go 一樣,幫助開發者從語言設計層面解決了這個問題,使得我們不用手動分配和釋放內存,這也是這類語言的優勢。

      本文主要解釋了:

      什么是內存管理,管理方式的方式

      Cpython 的內存管理方式

      垃圾回收機制

      Python 的引用計數、標記清楚和分代回收的垃圾自動回收方法。

      最后介紹了手動回收的包和為了提高內存有效使用的內存池機制

      希望看完這篇文章的讀者能對內存管理和垃圾回收有所興趣,下一篇文章再見~

      參考文章:

      對內存管理有興趣的強烈推薦閱讀: Memory Management in Python

      垃圾回收機制的算法與實現

      https://www.cnblogs.com/TM0831/p/10599716.html

      https://www.cnblogs.com/xybaby/p/7491656.html

      https://www.jianshu.com/p/c2c960481011

      Python

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:excel迷你圖空值沒有線段該怎么辦?
      下一篇:Word2007頁眉庫中自定義頁眉添加方法介紹(word如何自定義頁眉)
      相關文章
      亚洲国产成人精品无码一区二区| 亚洲精品国产高清在线观看| 亚洲精品国产综合久久久久紧| 日韩精品一区二区亚洲AV观看| 亚洲成a人片77777老司机| 亚洲成人影院在线观看| 性xxxx黑人与亚洲| 亚洲人成电影网站| 亚洲成人网在线观看| 亚洲天堂福利视频| 亚洲欧洲综合在线| 亚洲国产精品线观看不卡| 精品亚洲麻豆1区2区3区| 亚洲精品影院久久久久久| 亚洲欧洲日本天天堂在线观看| 亚洲视频.com| 亚洲日本国产精华液| 亚洲视频在线观看网址| 亚洲日韩在线视频| 亚洲国产日韩在线人成下载| 亚洲一级毛片在线播放| 亚洲高清中文字幕免费| 亚洲熟女综合一区二区三区| 亚洲爆乳无码专区www| 日韩色日韩视频亚洲网站| 亚洲av手机在线观看| 国产亚洲av人片在线观看| 亚洲欧洲美洲无码精品VA| 亚洲av激情无码专区在线播放| 久久久久久久亚洲Av无码| 亚洲国产精品白丝在线观看| 亚洲一区二区三区国产精华液| 亚洲精品无码久久久久久| 午夜亚洲福利在线老司机| 国产亚洲av片在线观看18女人| 国产亚洲精品xxx| 亚洲熟妇色自偷自拍另类| 亚洲欧美日韩自偷自拍| 一区二区三区亚洲视频| 亚洲男人第一无码aⅴ网站| 亚洲人成人无码网www电影首页|