趣談python的內存分配機制

趣談python的內存分配機制

原文地址:http://www.javashuo.com/article/p-uwslygwm-bd.html

雖然咱們如今得益於時代和技術的發展,不用再擔憂內存的問題;可是遙想當年,都是巴不得一個鋼鏰掰成倆份用,因此我就想深刻了解一下,在python中內存分配的一些小祕密。html

首先我會頻繁地用到sys模塊裏的getsizeof()方法,簡單介紹下:python

  • 該方法返回對象的字節大小(bytes)。
  • 它只計算直接佔用的內存,而不是計算對象內所引用對象的內存。

舉個例子:app

import sys

a = [1, 2]
b = [a, a]  # 即 [[1, 2], [1, 2]]

# a、b 都只有兩個元素,因此直接佔用的大小相等(只和元素個數有關,和元素是什麼無關)
sys.getsizeof(a) # 結果:80
sys.getsizeof(b) # 結果:80

上例說明了一件事:一個靜態建立的列表,若是隻包含兩個元素,那它自身佔用的內存就是 80 字節,無論其元素所指向的對象是什麼。rest

咱們已經知道如何測量了,那麼咱們就一塊兒來探索吧!code

一、空對象是「空」的嗎?

不知道你們有沒有好奇過,空的對象如空字符串、空列表、空字典,他們到底佔不佔用內存呢?若是佔用內存,又佔用多少呢?htm

直接揭曉答案吧:對象

import sys

print(sys.getsizeof(''))      # 49
print(sys.getsizeof([]))      # 64
print(sys.getsizeof(tuple())) # 48
print(sys.getsizeof(set()))   # 224
print(sys.getsizeof(dict()))  # 240

# 參照:
print(sys.getsizeof(None))    # 16
print(sys.getsizeof(False))   # 24
print(sys.getsizeof(0))       # 24
print(sys.getsizeof(1))       # 28
print(sys.getsizeof(True))    # 28

可見,雖然都是空對象,可是這些對象在內存分配上並不爲「空」,並且分配得還挺大(記住這幾個數字哦,後面會考)。blog

排序下就是:基礎數字<空元組 < 空字符串 < 空列表 < 空集合 < 空字典。排序

若是必定要解釋的話,個人理解就是:不一樣類在建立的時候自帶的屬性不同,如引用計數、使用量信息等等,還有一部份內存是預分配的。內存


二、內存擴充是均勻的嗎?

空對象並不爲空,一部分緣由是 Python 解釋器爲它們預分配了一些初始空間。在不超出初始內存的狀況下,每次新增元素,就使用已有內存,於是避免了再去申請新的內存。

那麼,若是初始內存被分配完以後,新的內存是怎麼分配的呢?

import sys
s = 'abcdefghijklmnopqrstuvwxyz'

a = []
for i in s:
    a.append(i)
    print(len(a) , f"a 的內存佔用{sys.getsizeof(a)}")

b = set()
for i in s :
    b.add(i)
    print(len(b),f"b 的內存佔用{sys.getsizeof(b)}")

c = dict()
for i in s :
    c[i] = i
    print(len(c) , f"c 的內存佔用{sys.getsizeof(c)}")

咱們給三類循環添加了26個元素,結果如何呢?

由此能看出可變對象在擴充時的祕密:

  • 超額分配機制:申請新內存時並非按需分配的,而是多分配一些,所以當再添加少許元素時,不須要立刻去申請新內存
  • 非均勻分配機制:三類對象申請新內存的頻率是不一樣的,而同一類對象每次超額分配的內存並非均勻的,而是逐漸擴大的

三、列表等於列表?

以上的可變對象在擴充時,有類似的分配機制,在動態擴容時可明顯看出效果。

那麼,靜態建立的對象是否也有這樣的分配機制呢?它跟動態擴容比,是否有所區別呢?

先看看集合與字典:

# 靜態建立對象
set_1 = {1, 2, 3, 4}
set_2 = {1, 2, 3, 4, 5}
dict_1 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5}
dict_2 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6}

sys.getsizeof(set_1)  # 224
sys.getsizeof(set_2)  # 736
sys.getsizeof(dict_1) # 240
sys.getsizeof(dict_2) # 368

看到這個結果,再對比上一節的截圖,能夠看出:在元素個數相等時,靜態建立的集合/字典所佔的內存跟動態擴容時徹底同樣。

這個結論是否適用於列表對象呢?一塊兒看看:

list_1 = ['a', 'b']
list_2 = ['a', 'b', 'c']
list_3 = ['a', 'b', 'c', 'd']
list_4 = ['a', 'b', 'c', 'd', 'e']

sys.getsizeof(list_1)  # 80
sys.getsizeof(list_2)  # 88
sys.getsizeof(list_3)  # 96
sys.getsizeof(list_4)  # 104

上一節的截圖顯示,列表在前 4 個元素時都佔 96 字節,在 5 個元素時佔 128 字節,與這裏明顯矛盾。

因此,這個祕密昭然若揭:在元素個數相等時,靜態建立的列表所佔的內存有可能小於動態擴容時的內存!

也就是說,這兩種列表看似相同,實際卻不一樣!列表不等於列表!

EMMM.....Interesting......


四、消減元素就會釋放內存?

前面提到了,擴充可變對象時,可能會申請新的內存。

那麼,若是反過來縮減可變對象,減掉一些元素後,新申請的內存是否會自動回收掉呢?

import sys
a = [1, 2, 3, 4]
sys.getsizeof(a) # 初始值:96
a.append(5)      # 擴充後:[1, 2, 3, 4, 5]
sys.getsizeof(a) # 擴充後:128
a.pop()          # 縮減後:[1, 2, 3, 4]
sys.getsizeof(a) # 縮減後:128

如代碼所示,列表在一擴一縮後,雖然回到了原樣,可是所佔用的內存空間可沒有自動釋放啊。其它的可變對象同理。

這就是 Python 的小祕密了,「胖子沒法減重原理」:瘦子變胖容易,縮減身型也容易,可是體重減不掉,哈哈~~~


四、空字典等於空字典?

使用 pop() 方法,只會縮減可變對象中的元素,但並不會釋放已申請的內存空間。

還有個 clear() 方法,它會清空可變對象的全部元素,讓咱們試試看吧:

import sys
a = [1, 2, 3]
b = {1, 2, 3}
c = {'a':1, 'b':2, 'c':3}

sys.getsizeof(a) # 88
sys.getsizeof(b) # 224
sys.getsizeof(c) # 240

a.clear()        # 清空後:[]
b.clear()        # 清空後:set()
c.clear()        # 清空後:{},也即 dict()

調用 clear() 方法,咱們就得到了幾個空對象。

在第一小節裏,它們的內存大小已經被查驗過了。(前面說過會考的,請默寫下)

可是,若是這時再查驗的話,你會驚訝地發現,這些空對象的大小跟前面查的並不徹底同樣!

# 承接前面的清空操做:
sys.getsizeof(a) # 64
sys.getsizeof(b) # 224
sys.getsizeof(c) # 72

空列表與空元組的大小不變,然而空字典(72)居然比前面的空字典(240)要小不少!

也就是說,列表與元組在清空元素後,回到起點不變初心,然而,字典這傢伙倒是「賠了夫人又折兵」,不只把「吃」進去的全吐出來了,還把本身的老本給虧掉了!

EMMM.....Interesting......

以上就是 Python 在分配內存時的幾個小祕密啦,看完以後,又能夠出去裝逼了呢!

若有心得或者疑問,歡迎留言一塊兒交流哦~

在這個大世界裏還有不少祕密等待着咱們去探索,一塊兒加油吧~!

相關文章
相關標籤/搜索