垃圾回收算法與實現系列-學習GC之前的準備工作

導語
  在學習垃圾回收算法之前,首先需要了解什麼是Heap、什麼是Root、什麼是Object、什麼是Stack、什麼是Pointer,這寫概念都是什麼,爲什麼要在垃圾回收算法中使用,使用這些東西有什麼樣的好處,爲什麼這樣使用。帶着這些問題來進入一個新的學習系列。


  首先我們知道Java是一個面向對象的編程語言,這裏首先需要了解的就是對象是什麼,對於對象的創建、使用、銷燬又是如何實現的。瞭解對象之後,就知道GC操作就是圍繞這些對象展開,而這些對象概括起來就是被應用程序所使用的數據集合。
  程序從開始運行就有很多的對象在內存空間中進行初始化操作,然後就是如何使用這些對象,在對象被應用程序使用完成之後,就進入到Java的垃圾回收機制,Java語言不需要使用者自己管理內存,通過自動內存管理的機制,在對象回收的過程中GC會根據這些對象的使用配置情況對對象進行移動或者銷燬,所以在GC中就必須要知道這些對象都是什麼。在GC中的對象主要包括兩部分內容對象頭header和field域。

對象結構

對象頭

  作爲一個對象來說首先,一個對象頭主要包含以下的一些信息

  • 對象的大小
  • 對象的種類

  這裏的對象頭就是用來標識對象的大小和對象的種類,對象的大小就是用來管理對象在內存中存儲邊界,而對象的類型就是控制在應用程序中如何使用這個對象,所以說對象頭,在整個的應用對象使用的過程中包括在後續的垃圾回收機制中都是非常重要的。
  由於看到在對象中有關於對象大小和種類等信息。然後在後續的GC過程中就可以根據GC算法的不同來存儲不同的信息。儘可能的保證就是高效的數據組織形式,也就是是根據合適的算法安排一個合適的數據存儲結構。例如在標記清除算法中,就在對象頭部設置了一個flag標識位來對對象進行標記管理。

  在所有的垃圾回收算法中都需要使用的內容就是對象的大小和對象種類信息,所以說這些基礎信息在對象創建的時候就會一直存在,而其他一些信息則是需要通過具體的回收算法來進行創建的。當然爲什麼會把這兩個東西作爲基礎信息呢?首先先來看看一段c語言的代碼

void* malloc (size_t size);

int *i = malloc(type,size)

  從上面代碼中,可以看到,在我們操作對象的時候如果不知道對象是什麼類型、對象佔用多大的內存空間就無法在堆內存中爲對象開闢合適的大小空間,如果不能在堆內存中開闢合適的大小空間,那麼對於後續的GC操作也是非常困難的。所以把對象大小和對象類型作爲基礎信息。

  在C語言中有一個概念叫做結構體,而對象中把對象可訪問的部分稱爲"域",也就是說這個域裏面纔是真正存儲使用的時候需要的信息。在C語言中使用結構體的時候都是以一個指針的形式存在,而Java語言中沒有指針的概念,把指針稱爲是引用。在實際使用對象的時候就會將這個引用替換爲具體的對象域值。從上面內容可以知道,在對象使用的過程中是沒有辦法直接去改變對象頭信息,也就是說一個對象一旦被創建之後,頭信息是無法直接改變的。所能變的就是域的信息。

  通過javap 命令反編譯Class文件的時候會有一個單詞Field,這個就表示域,也表示屬性,域就是使用者可以使用對象的部分,如果將其看看做是一個結構體的話域就表示結構體中的一個成員。在使用對象的時候會響應的引用或者是替換對象的域值。從String類型的對象來看。其實域中的內容可以分爲兩種類型

  • 指針
  • 非指針

 &emsp對於指針來講,它指向的內存中的一片內存區域。但是前面不是說Java是一個面向對象的編程語言麼,在Java中雖然沒有明確的使用指針這個概念,但是在語言的內部處理過程中是使用C語言進行處理。

  對於非指針,就可以看做是C語言中所提到的基本數據類型。

在這裏插入圖片描述

指針

  在GC操作的過程中,對象被保留或者被銷燬,這其中起到關鍵作用的就是指針,也就是平時我們口中所說的引用。在C或者C++語言中被叫做指針,在GC中它是根據這個對象的引用來進行搜索其他對象。那麼通過上面的概念可以知道,在域值中有指針也有非指針,那麼對於這些非指針又如何處理呢?在C語言中使用動態分配內存的時候,都需要給這塊內存進行一個初始化操作,有過C語言編程經驗的就會知道,在C語言動態開闢內存空間過後,那塊內存空間是有數據的,這是沒有辦法被清理掉的,所以在一般的情況下使用這塊內存的時候它會被新的數據所覆蓋,這個就是在C語言的使用中一定先要進行初始化操作,當然如果不初始化也可以,整個的使用過程中這些內存空間都會被新值所覆蓋了,所以,也就是說這裏對於非指針操作是不進行任何處理。

  既然有這樣的操作,有人也許會問既然有這樣的區別,那麼在執行java程序的時候會不會判斷指針和非指針呢?還是隻是在GC操作的時候會判斷。既然有指針的存在,那麼這個指針描述的是對象的哪個部分呢?如果是首地址這相對比較簡單,但是如果是其他地方的話就比較複雜了,在很多的情況下包括C語言中,都將指針默認指向對象的首地址。在GC中也是默認指向首地址。

在這裏插入圖片描述
  上面圖中可以將B對象和C對象看作是A對象的子對象。對某個對象以及其子對象進行處理是GC操作的基本操作。可以通過遞歸或者遍歷等操作獲取到子對象的指針數組如下,通過get函數就可以將B和C兩個對象依次的作爲參數傳入,而children()方法則是獲取到A對象的子對象數組。

for(child:children(A)){
	get(*child)
}

mutator

  mutator 英文的直譯是,突變,在GC操作的時候就是需要改變某些東西,通過上面的分析隱約的可以感覺到,這裏唯一可以變化的就是參與GC操作的對象的引用關係變化。也就是說在應用程序中,真個的垃圾回收需要有一個改變的東西,這個東西就被稱爲是mutator。對於這個操作主要有以下的兩個具體操作

  • 生成對象
  • 更新指針

  在mutator進行操作的時候,同時也會進行一些其他的處理,隨着這些處理就會發生對學習啊ing引用關係的改變,而由於對象引用關係發生了改變,有些對象就會變成無用的對象,這個時候就需要被垃圾回收機制所回收,而這裏負責整個的垃圾回收的機制就是GC操作。

  說到這裏mutator到底是什麼,它就是一個對象引用的搬用工,從創建到銷燬。

  在C語言中我們知道通過malloc()函數動態創建的的內存大小是被直接創建在真實的物理內存上的,那麼在Java中一個對象的創建操作,是被創建到JVM堆內存中的,也就是說堆就是一個動態的存放Java對象的內存空間。只不過這塊內存空間被專門進行了設計。當mutator 收到申請需要有對象進行存放的時候,所需要的內存就是從堆內存中進行分配的。這裏需要注意的一點是GC只管理已經被創建的對象,而mutator則是管理如何創建對象,如何更新對象指針。

  也就是說在mutator 開始之前,GC主要完成的操作是如何進行堆內存的分配,一旦mutator開始之後,就會按照mutator 要求的那樣在堆空間中存放對象,等到堆空間被放滿之後,GC就會進行清理,也就是說開始調整堆空間的的分配策略,如果不夠的話就要擴展堆空間的大小,直到達到利用–mxm 參數所設置的最大值,如果超過這個值沒有進行有效的策略就會出現內存溢出的情況。

  一般情況下在C語言中可以將內存看作爲一塊連續的空間。可以指定一塊大小爲HEAP_SIZE 的空間大小,這個就可以看做是堆,而要想使用它就需要一個指向這塊空間的首地址的指針。
在這裏插入圖片描述
  從上圖可以看到整個的三者之間的關係就如上圖所展示的一樣 heap_size = heap_start+HEAP_SIZE。

活動對象/非活動對象

  在很多場景中,將分配到內存空間中的所有對象中能通過mutator 引用的對象被稱作是活動對象,也就是說在應用程序中有對應的地方在使用這個對象,同樣的,如果被分配到內存中的對象不能被mutator 引用,那麼這些對象就是在應用程序中已經不使用了,這樣的情況被稱爲是非活動對象。而這些對象就應該是被垃圾回收機制所管理的對象。這裏在之前的博客中有一個概念就是四種引用,強引用,弱引用,虛引用,軟引用。這裏的mutator不能被引用就是這四種引用類型都無法引用。而這種對象就要被垃圾回收機制所處理,因爲這種狀態下的對象,就算是mutator 機制想要去引用,也沒有辦法從現有的引用鏈中找到。

在這裏插入圖片描述

分配

  前面提到一個概念就是動態分配,在C語言中爲了更好的使用內存,這裏通過malloc()函數與free()函數進行內存的管理,而在Java中所謂的分配則是在內存空間(這裏主要是指堆內存中)進行的對象的分配操作。當mutator 需要新的對象的時候,就會向分配管理器(allocator) 申請一個合適大小的空間,具體的僞代碼在上面的內容中已經寫過。分配器就需要在堆內存的可用空間中進行對象的分配。將最後的結果返回到mutator 中。

  當然這種操作也會遇到堆空間被佔用滿的時候,這個時候就算是執行了GC操作,也無法分配空間。那麼就需要有兩種操作

  • 1、擴展堆空間大小
  • 2、拋出異常

  根據實際的經驗擴展堆空間大小是最常用的邏輯,在虛擬機參數中有一個 --Xms參數和–Xmx參數分別表示

   -Xms<size>        設置初始 Java 堆大小
   -Xmx<size>        設置最大 Java 堆大小

  可見這裏採取的策略是繼續擴大堆內存,一般這個最大內存爲物理機實際內存,如果能消耗的把物理機內存都佔滿的話,那麼這個應用程序一般都是有問題的!

分塊

  在GC的操作中,分塊(chunk),是爲了高效利用對象而事先準備出來的一種內存空間。類似於JVM中Eden,From區,TO區等。這個與垃圾分代回收有關的內容。在初始狀態下,將所有的對象都分配到Eden區,然後在這個對象變爲垃圾對象,或者是出現回收操作之後,就進入到From區,To區,進一步的進行更高級的處理策略。整個的流程是
在這裏插入圖片描述

  Java編程語言是一個面向對象的編程語言,面向對象三大特點就是繼承、封裝、多態。我們知道在Java中所有的對象都有一個共同的父類Object,這裏根root 就是指這樣一個存在,它是所有對象的起點,也就是是通過mutator 的操作開始的地方

obj = new Object()

obj.field = SubObject();

  首先要實現一個公共根,這個對象一定是個共享對象,然後通過對象屬性將整個的調用鏈連接到一起。從上面的代碼中可以看到在field屬性被賦值爲SubObject之後,其實這個引用鏈路就被建立起來。當獲取活動對象的時候就從根開始,所有引用鏈路到達不了的對象,都是被稱爲非活動對象。
在這裏插入圖片描述
  GC會把通過上面直接或間接與根相連的對象看作是活動對象,而在整個的mutator操作的時候,而整個的調用過程都是通過棧(Stack)這種數據結構。也就是說它遵從的是先進後出,也就是通過深度優先遍歷,獲取到整個的調用鏈路。或者通過廣度優先遍歷獲取同一節點所有的引用鏈。

在這裏插入圖片描述

總結

  上面介紹了關於GC算法算法的一些常見的概念。在後續的分享中也會繼續使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章