自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Python內(nèi)存管理機(jī)制(Real Python版)

存儲(chǔ) 存儲(chǔ)軟件
是否曾想過(guò)Python怎樣在幕后管理數(shù)據(jù)?變量是怎樣存儲(chǔ)在內(nèi)存中的?什么時(shí)候會(huì)被刪除?在這篇文章中,我們將深入python內(nèi)部來(lái)探究?jī)?nèi)存管理。

[[256239]]

是否曾想過(guò)Python怎樣在幕后管理數(shù)據(jù)?變量是怎樣存儲(chǔ)在內(nèi)存中的?什么時(shí)候會(huì)被刪除?

在這篇文章中,我們將深入python內(nèi)部來(lái)探究?jī)?nèi)存管理。

讀完這篇文章,你將:

  • 了解更多關(guān)于底層計(jì)算邏輯,尤其是內(nèi)存相關(guān)方面
  • 理解Python怎樣對(duì)底層操作進(jìn)行抽象
  • 明白Python內(nèi)存管理的的算法

探究Python內(nèi)部原理能讓你有個(gè)更好的視角觀察Python。希望你對(duì)Python有個(gè)新的認(rèn)識(shí)。在你的程序正常運(yùn)行的背后有大量Python的功勞。

內(nèi)存是一本空白的書(shū)

首先,你可以把計(jì)算機(jī)的內(nèi)存想象成一本寫短篇故事的空白書(shū)。當(dāng)前的每一頁(yè)都是空的。不同的作者會(huì)參與進(jìn)來(lái)。每個(gè)作者都會(huì)得到一些頁(yè)面來(lái)寫入他們的故事。

他們得寫的很小心因?yàn)椴荒馨褨|西寫到其他人的頁(yè)面上,在他們寫之前,他們會(huì)和經(jīng)理商量一下。經(jīng)理來(lái)決定他們?cè)试S寫到哪些頁(yè)上。

因?yàn)檫@書(shū)寫了很久了,很多故事已經(jīng)沒(méi)什么意義了。當(dāng)一個(gè)故事沒(méi)人看或沒(méi)人提及時(shí),就會(huì)被刪掉,留下頁(yè)面給新的故事。

本質(zhì)上,計(jì)算機(jī)內(nèi)存就像那本空白的書(shū)。實(shí)際上,通常將固定長(zhǎng)度的連續(xù)內(nèi)存稱為內(nèi)存頁(yè),因此這個(gè)比喻很相似。

書(shū)的作者就像需要存數(shù)據(jù)到內(nèi)存的應(yīng)用或進(jìn)程。經(jīng)理決定作者可以在書(shū)中何處寫入內(nèi)容,他扮演著類似內(nèi)存管理器的角色。刪掉舊的故事給新故事騰出空白頁(yè)的人就是垃圾回收器。

內(nèi)存管理:從硬件到軟件

內(nèi)存管理是指應(yīng)用程序讀寫數(shù)據(jù)的過(guò)程。內(nèi)存管理器決定把應(yīng)用數(shù)據(jù)放在哪。因?yàn)閮?nèi)存是有限的,就跟前面書(shū)的比喻一樣,內(nèi)存管理器得找一些空位給程序。提供內(nèi)存空間的過(guò)程通常叫內(nèi)存分配。

另一方面,當(dāng)數(shù)據(jù)不再需要時(shí),可以被刪除或釋放。但釋放到哪里呢?這些內(nèi)存又是從哪里來(lái)的?

在計(jì)算機(jī)內(nèi)部,有個(gè)物理設(shè)備存儲(chǔ)著正在運(yùn)行的Python程序數(shù)據(jù)。在Python代碼和硬件之間隔著很多抽象層。

其中在硬件(比如內(nèi)存,硬盤)上面的最主要一層是操作系統(tǒng)。

操作系統(tǒng)之上就是程序了,其中就有Python的默認(rèn)實(shí)現(xiàn)版(內(nèi)置在操作系統(tǒng)或從python.org下載的)。Python代碼的內(nèi)存管理由Python程序負(fù)責(zé)的。Python程序用于內(nèi)存管理的算法和數(shù)據(jù)結(jié)構(gòu)就是本文的主旨。

Python的默認(rèn)實(shí)現(xiàn)版

Python的默認(rèn)實(shí)現(xiàn)版叫CPython,是C語(yǔ)言寫的。

***次知道的時(shí)候讓我很驚訝。一門語(yǔ)言由另一門語(yǔ)言編寫?!好吧,并不全是,但也差不多。

Python這門語(yǔ)言的定義是由英語(yǔ)寫在參考手冊(cè)上的。(https://docs.python.org/3/reference/index.html)

然而手冊(cè)本身并沒(méi)有什么很大作用。你仍需要按參考手冊(cè)中的規(guī)則寫出一些解析代碼。

注意:虛擬機(jī)就像硬件機(jī),但是由軟件實(shí)現(xiàn)的。

典型的基于指令的處理過(guò)程和匯編指令很相似。

Python是解釋執(zhí)行的語(yǔ)言。你的Python代碼實(shí)際上會(huì)被編譯成計(jì)算機(jī)更能識(shí)別的叫字節(jié)碼的指令。當(dāng)你運(yùn)行代碼的時(shí)候這些指令由虛擬機(jī)解析出來(lái)。

記得你見(jiàn)到的.pyc文件或__pycache__文件夾嗎?就是那些字節(jié)碼來(lái)被虛擬機(jī)解析。

同時(shí)你也需要能在計(jì)算機(jī)中實(shí)際執(zhí)行這些字節(jié)碼的東西。默認(rèn)的Python實(shí)現(xiàn)包含了這以上兩樣。需要了解的是除了CPython外還有很多其他實(shí)現(xiàn)。IronPython被編譯成在微軟的公共語(yǔ)言運(yùn)行時(shí)上運(yùn)行。Jython將編譯為Java字節(jié)碼在Java虛擬機(jī)上運(yùn)行。還有PyPy,關(guān)于它還得另起一篇文章,還是一筆帶過(guò)先。

這篇文章主要集中在Python默認(rèn)實(shí)現(xiàn)CPython是如何管理內(nèi)存上。

聲明:Python每個(gè)版本的發(fā)布都會(huì)有很多改變。

當(dāng)前篇幅主要討論的是Python3.7版。

說(shuō)回來(lái),CPython是用C寫的并可以解析Python字節(jié)碼。這些和內(nèi)存管理又有什么關(guān)聯(lián)呢?因?yàn)閮?nèi)存管理的算法和數(shù)據(jù)結(jié)構(gòu)就在C寫的CPython代碼里。要理解Python的內(nèi)存管理機(jī)制,就得對(duì)CPython本身有個(gè)基本的了解。

也許你聽(tīng)說(shuō)過(guò)在Python里面一切皆對(duì)象,包括像int或str這樣的類型本身。

注意:在C語(yǔ)言里面一個(gè)struct就是一組不同類型數(shù)據(jù)的集合。

可以類比為面向?qū)ο笳Z(yǔ)言里面一個(gè)只有屬性沒(méi)有方法的類。

CPython是用沒(méi)有原生面向?qū)ο笾С值腃語(yǔ)言寫的。所以,在CPython的代碼里面有很多有趣的設(shè)計(jì)。

PyObject在Python里面是所有對(duì)象的鼻祖,它只包含兩樣?xùn)|西:

  • ob_refcnt: 引用計(jì)數(shù)
  • ob_type: 類型指針

引用計(jì)數(shù)是用于垃圾回收的。類型指針則是指向另一實(shí)體類型的指針。那個(gè)類型的指針只是另一個(gè)描述Python實(shí)體的struct(比如dict或int)。

每個(gè)實(shí)體包含特定的內(nèi)存分配器,用于申請(qǐng)內(nèi)存和存儲(chǔ)自身。每個(gè)實(shí)體也有特定的內(nèi)存釋放器用于當(dāng)自身不被引用時(shí)的內(nèi)存釋放。

與此同時(shí),在申請(qǐng)和釋放內(nèi)存時(shí)還有個(gè)很重要的因素。內(nèi)存在計(jì)算機(jī)內(nèi)是共享資源,如果兩個(gè)不同的進(jìn)程同時(shí)使用一塊相同的內(nèi)存則會(huì)出錯(cuò)。

全局解釋器鎖(GIL)

GIL是解決計(jì)算機(jī)中如內(nèi)存之類的共享資源的通用解決方案。當(dāng)兩個(gè)線程試圖同時(shí)修改相同的資源時(shí),它們可能會(huì)影響到對(duì)方。最終結(jié)果可能是都得不到自己想要的結(jié)果。

再拿書(shū)本來(lái)打個(gè)比方。想象一下兩個(gè)固執(zhí)的作者堅(jiān)持本次該輪到自己來(lái)書(shū)寫。而且,他們寫的還是同一頁(yè)紙。

他們都忽略對(duì)方然后各自在這一頁(yè)上寫故事。

結(jié)果是兩個(gè)故事互相交織在一起,整頁(yè)都沒(méi)人看得懂。

有個(gè)解決方案是當(dāng)線程影響到共享資源(書(shū)本中的空白頁(yè))的時(shí)候有唯一一個(gè)全局的的鎖來(lái)鎖住解釋器。換句話說(shuō),同時(shí)只有一個(gè)作者可寫。

Python的GIL通過(guò)加鎖整個(gè)解釋器來(lái)獲得資格,就是說(shuō)另一個(gè)線程不會(huì)影響到當(dāng)前這個(gè)。當(dāng)CPython對(duì)內(nèi)存進(jìn)行處理的時(shí)候,使用GIL來(lái)確保這些操作是安全的。

這種方式有它的優(yōu)點(diǎn)和缺點(diǎn),Python社區(qū)對(duì)GIL的爭(zhēng)論很激烈。想要了解更多GIL的知識(shí),我建議你們可以看看《什么是全局解釋器鎖》(https://realpython.com/python-gil/?from=ethan)這篇文章。

垃圾回收

我們?cè)倏匆幌聲?shū)的類比,假設(shè)其中一些故事已經(jīng)過(guò)時(shí)了。沒(méi)人看也沒(méi)人引用這些故事。這種情況下就該處理掉這些故事以便騰出新的頁(yè)面。

這些沒(méi)人看和引用的故事就像Python里面引用計(jì)數(shù)為0的對(duì)象。提醒一下每個(gè)實(shí)體對(duì)象在Python中都有一個(gè)引用計(jì)數(shù)和類型指針。

有幾個(gè)不同的因素可讓引用計(jì)數(shù)增長(zhǎng)。比如,當(dāng)前對(duì)象被賦予其它變量時(shí)引用計(jì)數(shù)會(huì)增長(zhǎng)。

  1. numbers = [1, 2, 3] 
  2. # 引用計(jì)數(shù) = 1 
  3. more_numbers = numbers 
  4. # 引用計(jì)數(shù) = 2 

 

當(dāng)把對(duì)象傳參使用的時(shí)候也會(huì)增加引用計(jì)數(shù):

  1. total = sum(numbers) 

***舉個(gè)例子,當(dāng)一個(gè)list包含此對(duì)象的時(shí)候也會(huì)增加引用計(jì)數(shù):

  1. matrix = [numbers, numbers, numbers] 

你可以通過(guò)sys模塊來(lái)檢查Python對(duì)象的引用計(jì)數(shù)。你可以這樣用sys.getrefcount(numbers), 但要記得當(dāng)你用getrefcount()的時(shí)候numbers的引用計(jì)數(shù)也會(huì)加1。

任何情況下,如果一個(gè)對(duì)象仍在你代碼某處被使用,那它的引用計(jì)數(shù)就會(huì)大于0.一旦降為0的時(shí)候,這個(gè)對(duì)象特定的釋放函數(shù)就會(huì)被調(diào)用來(lái)釋放內(nèi)存給其他對(duì)象復(fù)用。

所謂的“釋放”到底是什么意思呢?其他對(duì)象又是如何復(fù)用這塊內(nèi)存的?讓我們深入CPython的內(nèi)存管理機(jī)制。

CPython的內(nèi)存管理機(jī)制

準(zhǔn)備好,我們即將深入研究CPython的內(nèi)存結(jié)構(gòu)和算法。

如上所述,在硬件和CPython之間還有很多抽象層。操作系統(tǒng)對(duì)實(shí)體內(nèi)存做了抽象并建立了一個(gè)虛擬內(nèi)存層給程序(包括Python)來(lái)訪問(wèn)。

Python留了一塊內(nèi)存來(lái)給對(duì)象之外的內(nèi)部使用。其他部分取決于對(duì)象如何存儲(chǔ)(int,dict等等)如果你想要個(gè)全面的了解,可以看下CPython的源碼,所有內(nèi)存管理相關(guān)的都在里面。

CPython有一個(gè)內(nèi)存分配器來(lái)負(fù)責(zé)在對(duì)象內(nèi)存區(qū)分配內(nèi)存。這個(gè)對(duì)象分配器就是所有魔法發(fā)生的源頭。每當(dāng)一個(gè)新的對(duì)象需要分配或釋放時(shí)都會(huì)被調(diào)用。

像典型的int或list等Python對(duì)象在每次分配和釋放時(shí)不會(huì)包含太多的數(shù)據(jù)。所以分配器被設(shè)計(jì)成在分配少批量數(shù)據(jù)時(shí)如何更好的工作。同時(shí)也要避免不要當(dāng)真的需要內(nèi)存的時(shí)候才去申請(qǐng)物理內(nèi)存。

源碼里面關(guān)于分配器的描述是:一種快速且為小塊內(nèi)存專用的分配器,用于通用malloc之上。此處講的malloc是C里面用于分配內(nèi)存的庫(kù)函數(shù)。

現(xiàn)在我們來(lái)看看CPython的內(nèi)存分配策略。首先,我們先講一下3個(gè)互相影響的區(qū)。

arenas區(qū)是內(nèi)存中***的區(qū),在內(nèi)存中是按頁(yè)對(duì)齊的。頁(yè)是指被操作系統(tǒng)使用的一小塊連續(xù)且固定大小的內(nèi)存塊。Python假設(shè)操作系統(tǒng)使用的頁(yè)大小是256K。

 

arenas區(qū)內(nèi)部是內(nèi)存池,每個(gè)內(nèi)存池是個(gè)虛擬內(nèi)存頁(yè)(4K)。就像我們類比書(shū)里面的空白頁(yè)面。這些內(nèi)存池被切分成更小的內(nèi)存塊。

同個(gè)內(nèi)存池內(nèi)的所有塊大小均相同。給定一組請(qǐng)求數(shù)據(jù),規(guī)格類定義了指定塊大小。以下圖表是從源碼注釋轉(zhuǎn)換而來(lái):

 

例如,如果需要42個(gè)字節(jié),那么數(shù)據(jù)會(huì)存放在一個(gè)48字節(jié)的塊中。

內(nèi)存池

內(nèi)存池是由相同規(guī)格類定義的塊組成。每個(gè)內(nèi)存池都管理著一個(gè)雙向鏈表,鏈接著其他相同規(guī)格的內(nèi)存池。由此算法可以很容易的通過(guò)給定的塊大小找到可用空間,甚至是在不同內(nèi)存池之間也行。

可通過(guò)已使用的內(nèi)存池列表追蹤所有相同規(guī)格類的可用空間。給定一個(gè)塊大小,算法可以從已使用內(nèi)存池列表中檢測(cè)出來(lái)。

內(nèi)存池必須是以下3種狀態(tài)之一:使用中,滿,空。使用中的內(nèi)存池有特定大小塊可供數(shù)據(jù)存儲(chǔ)。滿的內(nèi)存池內(nèi)被已分配的數(shù)據(jù)占滿??諆?nèi)存池沒(méi)有數(shù)據(jù),當(dāng)需要的時(shí)候可以被初始化為任意大小規(guī)格的內(nèi)存池。

空內(nèi)存池列表記錄著所有空狀態(tài)的內(nèi)存池。那空內(nèi)存池什么時(shí)候會(huì)被用到呢?

假設(shè)你的代碼需要8個(gè)字節(jié)的內(nèi)存池塊。如果在已使用的內(nèi)存池列表中沒(méi)有關(guān)于8個(gè)字節(jié)規(guī)格的,那么一個(gè)空的內(nèi)存池會(huì)被初始化為專門存儲(chǔ)8個(gè)字節(jié)。同時(shí)這個(gè)新的內(nèi)存池會(huì)被添加到已使用內(nèi)存池中供接下來(lái)的請(qǐng)求使用。

當(dāng)滿的內(nèi)存池當(dāng)中有些塊被回收了,那么這個(gè)內(nèi)存池又會(huì)被添加到當(dāng)前大小的使用中內(nèi)存池列表中。

現(xiàn)在你知道這些內(nèi)存池是怎樣從不同狀態(tài)之間自由切換的算法了。

內(nèi)存塊

 

由上圖可知,內(nèi)存池包含一個(gè)指向空內(nèi)存塊的指針。這里有一點(diǎn)細(xì)微的差別。源代碼的注釋指出,分配器力求在各級(jí)別(arena, pool, block)內(nèi)存真正被需要的時(shí)候才去使用它。

內(nèi)存池中的塊有3種狀態(tài)。這些狀態(tài)的定義如下:

  • untouched: 還未被分配使用的內(nèi)存塊
  • free: 被分配然后又被"釋放"的內(nèi)存塊且里面沒(méi)有保存相關(guān)數(shù)據(jù)了
  • allocated: 已分配且含有數(shù)據(jù)的內(nèi)存塊

free狀態(tài)的塊指針列表保存著一系列的free態(tài)內(nèi)存。換句話說(shuō),一個(gè)可用來(lái)放數(shù)據(jù)的列表。如果需要比可用的所有free態(tài)內(nèi)存還要多,那么分配器會(huì)去使用那些untouched態(tài)的塊。

當(dāng)內(nèi)存管理器把內(nèi)存塊狀態(tài)置為"釋放"時(shí)會(huì)把它添加到free態(tài)鏈表的頭部。這個(gè)鏈表可能不像上面那圖一樣為連續(xù)的內(nèi)存塊。它可能是如下圖那樣:

 

Arenas區(qū)

arenas區(qū)包含著內(nèi)存池。這些內(nèi)存池可以是使用中,滿,或空的。arenas區(qū)不像內(nèi)存池那樣有明顯的狀態(tài)區(qū)分。

arenas區(qū)由稱為usable_arenas的雙向鏈表組織而成。此鏈表按可用內(nèi)存池的數(shù)量排序。越少可用內(nèi)存池的排在越前面。

 

這意味著arena區(qū)會(huì)選擇更接近用滿的地方來(lái)存放數(shù)據(jù)。為什么不反過(guò)來(lái)做呢?為什么數(shù)據(jù)不放到最空的地方去?

這就要說(shuō)到真正的內(nèi)存釋放。你也許注意到我給釋放加了引號(hào), 它并不是真正的釋放到操作系統(tǒng)。Python繼續(xù)保留著以供新的數(shù)據(jù)使用。真正的內(nèi)存釋放是返回給操作系統(tǒng)使用。

arenas區(qū)是唯一可以真正被釋放的地方。所以那些接近為空的區(qū)域也理所當(dāng)然應(yīng)當(dāng)為空。通過(guò)這種方式,可以真正釋放內(nèi)存,減少Python程序的總體內(nèi)存占用。

總結(jié)

內(nèi)存管理是計(jì)算機(jī)工作中不可或缺的一部分。不管好壞,Python幾乎在幕后處理所有這些問(wèn)題。

在本篇中,你學(xué)到了:

  • 什么是內(nèi)存管理和為什么它很重要
  • 默認(rèn)的Python實(shí)現(xiàn)CPython是用C寫的
  • CPython的內(nèi)存管理是怎樣通過(guò)數(shù)據(jù)結(jié)構(gòu)和算法來(lái)管理你的數(shù)據(jù)的

Python抽象了很多繁雜的細(xì)節(jié)來(lái)與計(jì)算機(jī)打交道,讓你有能力從更高的層次來(lái)開(kāi)發(fā)代碼而不用為字節(jié)存放到哪而頭疼。

責(zé)任編輯:武曉燕 來(lái)源: 假構(gòu)師
相關(guān)推薦

2010-07-23 09:34:48

Python

2022-02-28 10:25:17

Python參數(shù)傳遞拷貝

2013-09-29 15:11:46

Linux運(yùn)維內(nèi)存管理

2022-06-01 16:01:58

MySQL內(nèi)存管理系統(tǒng)

2010-09-26 13:23:13

JVM內(nèi)存管理機(jī)制

2010-12-10 15:40:58

JVM內(nèi)存管理

2011-06-29 17:20:20

Qt 內(nèi)存 QOBJECT

2020-11-08 14:32:01

JavaScript變量內(nèi)存管理

2016-10-09 14:41:40

Swift開(kāi)發(fā)ARC

2020-08-18 19:15:44

Redis內(nèi)存管理

2009-10-22 17:39:34

CLR內(nèi)存管理

2011-08-18 13:28:35

Objective-C內(nèi)存

2010-09-27 13:26:31

JVM內(nèi)存管理機(jī)制

2009-09-02 09:23:26

.NET內(nèi)存管理機(jī)制

2010-01-06 10:23:47

.NET Framew

2009-07-08 15:10:00

Servlet會(huì)話管理

2021-02-07 09:02:28

內(nèi)存管理length

2016-09-06 22:05:41

HttpCookieWeb

2009-09-23 17:48:00

Hibernate事務(wù)

2021-12-15 06:58:27

Go多版本管理
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)