1. 程式人生 > 其它 >Java-IO流系列-隨機存取檔案流

Java-IO流系列-隨機存取檔案流

垃圾回收機制

一句話說就是: 引用計數器為主,分程式碼回收和標記清除為輔

1.1大管家refchain

在python的C原始碼中有一個名為refchain的_環狀雙向連結串列_,python中一旦建立物件就會把這個物件新增到refchain這個連結串列中.也就是儲存著所有的物件.

例如:

age = 18
name = "aa"

1.2引用計數器

在refchain中的所有物件內部都有一個_ob_refcnt_用來儲存當前物件的引用計數器,顧名思義就是自己被引用的次數,例如:

age = 18
name = "aa"
nickname = name

上述程式碼表示記憶體中有18和"aa"兩個值,他們的引用計數器分別為:1,2.

當值被多次引用時候,不會在記憶體中重複建立資料,而是_引用計數器+1_.當物件被銷燬時候同時會讓_引用計數器-1_,如果引用計數器為0,則將物件從refchain連結串列中拆除,同時在記憶體中進行銷燬(暫不考慮快取等特殊情況)

age = 18
number = age #物件18的引用計數器+1
del age #物件18的引用計數器-1

def run(arg):
	print(arg)

run(number) #剛開始執行函式時,物件18引用計數器+1,當函式執行完畢之後,物件18引用計數器-1

num_list = [11,22,number] #物件18的引用計數器+1

1.3標記清除&分代回收

基於引用計數器進行垃圾回收非常方便和簡單,但它還是存在_迴圈引用_的問題,導致無法正常的回收一些資料,例如:

v1 = [11,22,33] #refchain 中建立一個列表物件,由於v1=物件,所以列表引用物件用計數器為1.
v2 = [44,55,66] #refchain 中再建立一個列表物件,因v2 = 物件,所以列表物件引用計數器為1.
v1.append(v2) #把v2追加到v1中,則v2對應的[44,55,66]物件的引用計數器加1,最終為2.
v2.append(v1) #把v1追加到v2中,則v1對應的[11,22,33]物件的引用計數器加1,最終為2.

del v1 #引用計數器-1
del v2 #引用計數器-1

對於上述程式碼,執行_del_操作之後,沒有變數再會去使用那兩個列表物件,但由於迴圈引用的問題,他們的引用計數器不為0,所以他們的狀態,永遠不會被使用,也不會被銷燬.專案中如果這種程式碼太多,就會導致記憶體一直被消耗,直到記憶體被耗盡,程式崩潰.

為了解決迴圈引用的問題,引入了_標記清除_技術,專門針對那些可能存在迴圈引用的物件進行特殊處理,可能存在迴圈應用的型別有:列表,元組,字典,集合,自定義類等那些能進行資料巢狀的型別.

標記清除:建立特殊連結串列專門用於儲存列表,元組,字典,集合,自定義類等物件,之後再去檢查這個連結串列中的物件是否存在迴圈引用,如果存在則讓對方的引用計數器均-1.

分代回收:對標記清除中的連結串列進行優化,將那些可能存在循引用的物件拆分到3個連結串列,連結串列稱為:0/1/2三代,每代都可以儲存物件和閾值,當達到閾值時,就會對相應的連結串列中的每個物件做一次掃描,除迴圈引用各自減1並且銷燬引用計數器為0的物件.

特別注意:0代和1,2代的threshold和count表示的意義不同.

  • 0代,count表示0代連結串列中物件的數量,threshold表示0代連結串列物件個數閾值,超過則執行一次0代掃描檢查.
  • 1代,count表示0代連結串列掃描的次數,threshold表示0代連結串列掃描的次數閾值,超過則執行一次1代掃描檢查.
  • 2代,count表示1代連結串列掃描的次數,threshold表示1代連結串列掃描的次數閾值,超過則執行--2代掃描檢查.

1.4情景模擬

第一步:當建立物件age = 19時,會將物件新增到refchain連結串列中

第二步:當建立物件num_list = [11,22]時,會將列表物件新增到refchain和generations0代中

第三步:新建立物件使generations的0代連結串列上的物件數量大於閾值700時,要對連結串列上的物件進行掃描檢查.

當0代大於閾值後,底層不是直接掃描0代,而是先判斷2,1是否也超過了閾值.

  • 如果2,1代未達到閾值,則掃描0代,並讓1代的count+1.
  • 如果2代已達到閾值,則將2,1,0三個連結串列拼接起來進行全掃描,並將2,1,0代的count重置為0
  • 如果1代已達到閾值,則將1,0兩個連結串列拼接起來進行掃描,並將所有1,0代的count重置為0

對拼接起來的連結串列進行掃描時,主要就是剔除迴圈引用和銷燬垃圾,詳細過程為:

  • 掃描連結串列,把每個物件的引用計數器拷貝一份並儲存到gc_refs中,保護原引用計數器.
  • 再次掃描連結串列中的每個物件,並檢查是否存在迴圈引用,如果存在則讓各自的gc_refs減1.
  • 再次掃描連結串列,將gc_refs為0的物件移動到unreachable連結串列中,不為0的物件直接升級到下一代連結串列中
  • 處理unreachable連結串列中的物件的解構函式和弱引用,不能被銷燬的物件升級到下一代連結串列,能銷燬的保留在此連結串列.
    • 解構函式,指的就是那些定義了__del__方法的物件,需要執行之後再進行銷燬處理.
    • 弱引用
  • 最後將unreachable中的每個物件銷燬並再refchain連結串列中移除(不考慮快取機制)

至此,垃圾回收的過程結束

1.5快取機制

因為反覆的建立和銷燬會使程式的執行效率變低,所以python中引入了"快取機制"

例如:引用計數器為0時,不會真正銷燬物件,而是將它放到一個名為free_list的連結串列中,之後再建立物件時不會再重新開闢記憶體,而是在free_list中將之前的物件拿來並重置內部的值進行使用.

  • float型別,維護的free_list連結串列最多可快取100個float物件.

    v1 = 3.14 #開闢記憶體來儲存float物件,並將物件新增到refchain連結串列
    print(id(v1)) #記憶體地址: 4436033488
    del v1 #引用計數器-1 如果為0則在rechain連結串列中移除,不銷燬物件,而是將物件新增到float的free_list.
    v2 = 9.999 #優先去free_list中獲取物件,並重置為9.999,如果free_list為空才重新開闢記憶體.
    print(id(v2))
    
    #注意:已引用計數器為0時,會先判斷free_list中快取個數是否滿了,未滿則將物件快取,已滿則直接將物件銷燬
    
  • int型別,不是基於free_list,而是維護一個small_ints連結串列儲存常見資料(小資料池),小資料池範圍:-5<=value<257.即:重複使用這個範圍的整數時,不會重新開闢記憶體.

    v1= 38 #去小資料池small_ints中獲取38整數物件,將物件新增到refchain並讓引用計數器+1
    print(id(V1)) #記憶體地址:4514343712
    v2= 38 #去小資料池small_ints中獲取38整數物件,將refchain中的物件的引用計數器+1.
    print(id(v2)) #記憶體地址:4514343712
    
    #注意:在直譯器啟動時-5~256就已經被加入到small_ints連結串列中且引用計數器初始化為1,程式碼中使用的值直接去small_ints中拿來並將引用計數器+1即可.另外,small_ints中的資料引用計數器永遠不會為0(初始化時就設定為1了),所以也不會被銷燬
    
  • str型別,維護unicode_latin1[256]連結串列,內部將所有的ascii字元快取起來,以後使用時就不再反覆建立

    v1 = "A"
    print(id(v1)) #輸出:4517720496
    del v1
    v2 = "A"
    print(id(v1)) #輸出:4517720496
    
    #除此之外,python內部還對字串做了駐留機制,針對只含有字母,數字,下劃線的字串,如果記憶體中已存在則不會重新建立而是使用原來的地址裡(不會像free_list那樣一直在記憶體存活,只有記憶體中有才能被重複利用)
    v1 = "aa"
    v2 = "aa"
    print(id(v1)==id(v2)) #輸出:True
    
  • list型別,維護的free_list陣列最多可快取80個list物件.

    v1= [11,22,33]
    print(id(v1)) #輸出:4517628816
    del v1
    v2 = ["a","b"]
    print(id(v2)) #輸出:4517628816
    
  • tuple型別,維護一個free_list陣列且陣列容量20,陣列中元素可以是連結串列且每個連結串列最多可以容納2000個元組物件.元組的free_list陣列在儲存資料時,是按照元組可以容納的個數為索引找到free_list陣列中對應的連結串列,並新增到連結串列中.

    v1 = (1,2)
    print(id(v11))
    del v1 #因元組的數量為2,所以會把這個物件快取到free_list[2]的連結串列中
    v2 = ("a","b") #不會重新開闢記憶體,而是去free_list[2]對應的連結串列中拿到一個物件來使用.
    print(id(v2))
    
  • dict型別,維護的free_list陣列最多可快取80個dict物件.

    v1= {"k1":123}
    print(id(v1)) #輸出:4515998128
    del v1
    v2 = {"name":"a","age":15,"gender":"男"}
    print(id(v2)) #輸出:451598128