Java 底層機制(JVM/堆/棧/方法區/GC/類載入)

FrankYou發表於2018-05-15

轉載:https://www.jianshu.com/p/ae97b692614e?from=timeline

JVM體系結構

JVM是一種解釋執行class檔案的規範技術。

 
JVM體系結構

 

我翻譯的中文圖:

 
中文圖

類裝載器子系統

在JVM中負責裝載.class檔案(一種8位二進位制流檔案,各個資料項按順序緊密的從前向後排列, 相鄰的項之間沒有間隙,經編譯器編譯.java原始檔後生成,每個類(或者介面)都單獨佔有一個class檔案)。

執行時資料區

方法區

JVM使用類裝載器定位class檔案,並將其輸入到記憶體中時。會提取class檔案的型別資訊,並將這些資訊儲存到方法區中。同時放入方法區中的還有該型別中的類靜態變數

  • 該型別的全限定名。如java.io.FileOutputStream
  • 該型別的直接超類的全限定名。如java.io.OutputStream
  • 該型別是類型別還是介面型別。
  • 該型別的訪問修飾符(publicabstractfinal)。
  • 任何直接超介面的全限定名的有序列表。如java.io.Closeable, java.io.Flushable
  • 該型別的常量池。比如所有型別(Class)、方法、欄位的符號、基本資料型別的直接數值(final)等。
  • 欄位資訊:對型別中宣告的每個欄位。
  • 方法資訊
  • 類靜態變數:靜態變數而不是放在堆裡面,所以靜態屬於類,不屬於物件
  • 指向ClassLoader類的引用。
  • 指向Class類的引用。
  • 方法表:為了能快速定位到型別中的某個方法,JVM對每個裝載的型別都會建立一個方法表,用於儲存該型別物件可以呼叫的方法的直接引用,這些方法就包括從超類中繼承來的。而這張表與Java動態繫結機制的實現是密切相關的。

常量池

常量池指的是在編譯期被確定,並被儲存在已編譯的.class檔案中的一些資料。除了包含程式碼中所定義的各種基本資料型別和物件型(String及陣列)的常量值(final,在編譯時確定,並且編譯器會優化)還包含一些以文字形式出現的符號引用(類資訊),比如:

  • 類和介面的全限定名
  • 欄位的名稱和描述符
  • 方法和名稱和描述符

虛擬機器必須給每個被裝載的型別維護一個常量池。常量池就是該型別所用到常量的一個有序集合,包括直接常量(string、integer等)和其他型別,欄位和方法的符號引用

方法區是多執行緒共享的。也就是當虛擬機器例項開始執行程式時,邊執行邊載入進class檔案。不同的Class檔案都會提取出不同型別資訊存放在方法區中。同樣,方法區中不再需要執行的型別資訊會被垃圾回收執行緒丟棄掉

堆記憶體

Java 程式在執行時建立的所有型別物件和陣列都儲存在堆中JVM會根據new指令在堆中開闢一個確定型別的物件記憶體空間。但是堆中開闢物件的空間並沒有任何人工指令可以回收,而是通過JVM的垃圾回收器負責回收

  • 堆中物件儲存的是該物件以及物件所有超類的例項資料(但不是靜態資料)。
  • 其中一個物件的引用可能在整個執行時資料區中的很多地方存在,比如Java棧,堆,方法區等
  • 堆中物件還應該關聯一個物件的鎖資料資訊以及執行緒的等待集合(執行緒等待池)。這些都是實現Java執行緒同步機制的基礎。
  • java中陣列也是物件,那麼自然在堆中會儲存陣列的資訊。

程式計數器

對於一個執行的Java而言,每一個執行緒都有一個PC暫存器。當執行緒執行Java程式時,PC暫存器的內容總是下一條將被執行的指令地址

Java棧

每啟動一個執行緒JVM都會為它分配一個Java棧,用於存放方法中的區域性變數,運算元以及異常資料等。當執行緒呼叫某個方法時,JVM會根據方法區中該方法的位元組碼組建一個棧幀。並將該棧幀壓入Java棧中,方法執行完畢時,JVM會彈出該棧幀並釋放掉。

注意Java棧中的資料是執行緒私有的,一個執行緒是無法訪問另一個執行緒的Java棧的資料。這也就是為什麼多執行緒程式設計時,兩個相同執行緒執行同一方法時,對方法內的區域性變數是不需要資料同步的原因

java棧和區域性變數詳解

成員變數有預設值(被final修飾且沒有static的必須顯式賦值),區域性變數不會自動賦值

執行引擎

執行Java的每一個執行緒都是一個獨立的虛擬機器執行引擎的例項。從執行緒生命週期的開始到結束,他要麼在執行位元組碼,要麼在執行本地方法。一個執行緒可能通過解釋或者使用晶片級指令直接執行位元組碼,或者間接通過JIT(即時編譯器)執行編譯過的原生程式碼。

注意JVM是程式級別,執行引擎是執行緒級別

指令集

實際上,class檔案中方法的位元組碼流就是有JVM的指令序列構成的。每一條指令包含一個單位元組的操作碼,後面跟隨0個或多個運算元

指令由一個操作碼和零個或多個運算元組成。

iload_0    // 把儲存在區域性變數區中索引為0的整數壓入運算元棧。
iload_1    // 把儲存在區域性變數區中索引為1的整數壓入運算元棧。
iadd         // 從運算元棧中彈出兩個整數相加,在將結果壓入運算元棧。  
istore_2   // 從運算元棧中彈出結果  

很顯然,上面的指令反覆用到了Java棧中的某一個方法棧幀。實際上執行引擎執行Java位元組碼指令很多時候都是在不停的操作Java棧,也有的時候需要在堆中開闢物件以及執行系統的本地指令等。但是Java棧的操作要比堆中的操作要快的多,因此反覆開闢物件是非常耗時的。這也是為什麼Java程式優化的時候,儘量減少new物件。

示例分析

//原始碼 Test.java  
package edu.hr.jvm;  
  
import edu.hr.jvm.bean;  
public class Test{  
       public static void main(String[] args){  
               Act act=new Act();  
               act.doMathForever();  
       }  
}  
  
//原始碼 Act.java  
package edu.hr.jvm.bean;  
  
public class Act{  
       public void doMathForever(){  
              int i=0;  
              for(;;){  
                     i+=1;  
                     i*=2;   
              }  
       }  
}  

 

 
示例
  • 首先OS會建立一個JVM例項(進行必要的初始化工作,比如:初始啟動類裝載器,初始執行時記憶體資料區等。

  • 然後通過自定義類裝載器載入Test.class。並提取Test.class位元組碼中的資訊存放在方法區 中(具體的資訊在上面已經講過)。上圖展示了方法區中的Test類資訊,其中在常量池中有一個符號引用“Act”(類的全限定名,注意:這個引用目前還沒有真正的類資訊的記憶體地址)。

  • 接著JVM開始從Test類的main位元組碼處開始解釋執行。在執行之前,會在Java棧中組建一個main方法的棧幀 ,如上圖Java棧所示。JVM需要執行任何方法前,通過在Java棧中壓入一個幀棧。在這個幀棧的記憶體區域中進行計算。

  • 現在可以開始執行main方法的第一條指令 —— JVM需要為常量池的第一項的類(符號引用Act)分配記憶體空間。但是Act類此時還沒有載入進JVM(因為常量池目前只有一個“Act”的符號引用)。

  • JVM載入進Act.class,並提取Act類資訊放入方法區中。然後以一個直接指向方法區Act類資訊的直接引用(在棧中)換開始在常量池中的符號引用“Act”,這個過程就是常量池解析 。以後就可以直接訪問Act的類資訊了。

  • 此時JVM可以根據方法區中的Act類資訊,在堆中開闢一個Act類物件act

  • 接著開始執行main方法中的第二條指令呼叫doMathForever方法。這個可以通過堆中act物件所指的方法表中查詢,然後定位到方法區中的Act類資訊中的doMathForever方法位元組碼。在執行之前,仍然要組建一個doMathForever棧幀壓入Java棧。(注意:JVM會根據方法區中doMathForever的位元組碼來建立棧幀的區域性變數區運算元棧的大小)

  • 接下來JVM開始解釋執行Act.doMathForever位元組碼的內容了。

編譯和執行過程

編譯:原始碼要執行,必須先轉成二進位制的機器碼。這是編譯器的任務。

  • 原始檔由編譯器編譯成位元組碼。 建立完原始檔之後,程式會先被編譯為.class檔案。Java編譯一個類時,如果這個類所依賴的類還沒有被編譯,編譯器就會先編譯這個被依賴的類,然後引用,否則直接引用。如果java編譯器在指定目錄下找不到該類所其依賴的類的.class檔案或者.java原始檔的話,編譯器話報“cant find symbol”的錯誤。
  • 編譯後的位元組碼檔案格式主要分為兩部分:常量池和方法位元組碼。常量池記錄的是程式碼出現過的所有token(類名,成員變數名等等)以及符號引用(方法引用,成員變數引用等等);方法位元組碼放的是類中各個方法的位元組碼。

執行java類執行的過程大概可分為兩個過程:類的載入,類的執行。需要說明的是:JVM主要在程式第一次主動使用類的時候,才會去載入該類。也就是說,JVM並不是在一開始就把一個程式就所有的類都載入到記憶體中,而是到不得不用的時候才把它載入進來,而且只載入一次

下面是程式執行的詳細步驟


//MainApp.java  
public class MainApp {  
    public static void main(String[] args) {  
        Animal animal = new Animal("Puppy");  
        animal.printName();  
    }  
}  
//Animal.java  
public class Animal {  
    public String name;  
    public Animal(String name) {  
        this.name = name;  
    }  
    public void printName() {  
        System.out.println("Animal ["+name+"]");  
    }  
} 
  • 在編譯好java程式得到MainApp.class檔案後,在命令列上敲java AppMain。系統就會啟動一個jvm程式jvm程式classpath路徑中找到一個名為AppMain.class的二進位制檔案,將MainApp的類資訊載入到執行時資料區的方法區內,這個過程叫做MainApp類的載入
  • 然後JVM找到AppMain的主函式入口,開始執行main函式
  • main函式的第一條命令是Animal animal = new Animal("Puppy");就是讓JVM建立一個Animal物件,但是這時候方法區中沒有Animal類的資訊,所以JVM馬上載入Animal類,把Animal類的型別資訊放到方法區中。
  • 載入完Animal類之後,Java虛擬機器做的第一件事情就是在堆區中為一個新的Animal例項分配記憶體,然後呼叫建構函式初始化Animal例項,這個Animal例項持有著指向方法區的Animal類的型別資訊(其中包含有方法表,java動態繫結的底層實現)的引用。
  • 當使用animal.printName()的時候,JVM根據animal引用找到Animal物件,然後根據Animal物件持有的引用定位到方法區中Animal類的型別資訊的方法表,獲得printName()函式的位元組碼的地址。
  • 開始執行printName()函式的位元組碼(可以把位元組碼理解為一條條的指令)。
     
    圖示

特別說明:java類中所有public和protected的例項方法都採用動態繫結機制,所有私有方法靜態方法構造器初始化方法<clinit>都是採用靜態繫結機制。而使用動態繫結機制的時候會用到方法表,靜態繫結時並不會用到。

通過前面的兩個例子的分析,應該理解了不少了吧。

類載入機制

JVM主要包含三大核心部分:類載入器,執行時資料區和執行引擎

虛擬機器將描述類的資料從class檔案載入到記憶體,並對資料進行校驗,準備,解析和初始化,最終就會形成可以被虛擬機器使用的java型別,這就是一個虛擬機器的類載入機制。java在類中的類是動態載入的,只有在執行期間使用到該類的時候,才會將該類載入到記憶體中,java依賴於執行期動態載入和動態連結來實現類的動態使用。

一個類的生命週期:

 
Paste_Image.png

載入,驗證,準備,初始化和解除安裝在開始的順序上是固定的,但是可以交叉進行。
在Java中,對於類有且僅有四種情況會對類進行“初始化”。

  • 使用new關鍵字例項化物件的時候,讀取或設定一個類的靜態欄位時候(除final修飾的static外),呼叫類的靜態方法時候,都只會初始化該靜態欄位或者靜態方法所定義的類
  • 使用reflect包對類進行反射呼叫的時候,如果類沒有進行初始化,則先要初始化該類。
  • 當初始化一個類的時候,如果其父類沒有初始化過,則先要觸發其父類初始化。
  • 虛擬機器啟動的時候,會初始化一個有main方法的主類

注意

  • 子類引用父類靜態欄位,只會初始化父類不會初始化子類
  • 通過陣列定義來引用類,也不會觸發該類的初始化
  • 常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接引用到定義常量的類,因此也不會觸發定義常量的類的初始化

類載入過程

載入

載入階段主要完成三件事,即通過一個類的全限定名來獲取定義此類的二進位制位元組流,將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構,在Java堆中生成一個代表此類的Class物件,作為訪問方法區這些資料的入口。這個載入過程主要就是靠類載入器實現的,這個過程可以由使用者自定義類的載入過程。

驗證
這個階段目的在於確保才class檔案的位元組流中包含資訊符合當前虛擬機器要求,不會危害虛擬機器自身安全。
主要包括四種驗證:

  • 檔案格式驗證:基於位元組流驗證,驗證位元組流是否符合Class檔案格式的規範,並且能被當前虛擬機器處理。
  • 後設資料驗證:基於方法區的儲存結構驗證,對位元組碼描述資訊進行語義驗證。
  • 位元組碼驗證:基於方法區的儲存結構驗證,進行資料流和控制流的驗證。
  • 符號引用驗證:基於方法區的儲存結構驗證,發生在解析中,是否可以將符號引用成功解析為直接引用。

準備
僅僅為類變數(即static修飾的欄位變數)分配記憶體並且設定該類變數的初始值即零值,這裡不包含用final修飾的static,因為final在編譯的時候就會分配了(編譯器的優化),同時這裡也不會為例項變數分配初始化。類變數會分配在方法區中,而例項變數是會隨著物件一起分配到Java堆中。

解析
解析主要就是將常量池中的符號引用替換為直接引用的過程。符號引用就是一組符號來描述目標,可以是任何字面量,而直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制程式碼。有類或介面的解析,欄位解析,類方法解析,介面方法解析。

初始化
初始化階段依舊是初始化類變數和其他資源,這裡將執行使用者的static欄位和靜態語句塊的賦值操作。這個過程就是執行類構造器< clinit >方法的過程。
< clinit >方法是由編譯器收集類中所有類變數的賦值動作和靜態語句塊的語句生成的,類構造器< clinit >方法與例項構造器< init >方法不同,這裡面不用顯示的呼叫父類的< clinit >方法,父類的< clinit >方法會自動先執行於子類的< clinit >方法。即父類定義的靜態語句塊和靜態欄位都要優先子類的變數賦值操作。

類載入器

類載入器的分類

  • 啟動類載入器(Bootstrap ClassLoader):主要負責載入<JAVA_HOME>\lib目錄中的'.'或是-Xbootclasspath引數指定的路徑中的,並且可以被虛擬機器識別(僅僅按照檔名識別的)的類庫到虛擬機器記憶體中。它載入的是System.getProperty("sun.boot.class.path")所指定的路徑jar
  • 擴充套件類載入器(Extension ClassLoader):主要負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。它載入的是
    System.getProperty("java.ext.dirs")所指定的路徑或jar
  • 應用程式類載入器(Application ClassLoader):也叫系統類載入器,主要負責載入ClassPath路徑上的類庫,如果應用程式沒有自定義自己類載入器,則這個就是預設的類載入器。它載入的是System.getProperty("java.class.path")所指定的路徑jar

類載入器的特點

  • 執行一個程式時,總是由Application Loader(系統類載入器)開始載入指定的類。
  • 在載入類時,每個類載入器會將載入任務上交給其父,如果其父找不到,再由自己去載入。
  • Bootstrap Loader(啟動類載入器)是最頂級的類載入器了,其父載入器為null

類載入器的雙親委派模型

類載入器雙親委派模型的工作過程是:如果一個類載入器收到一個類載入的請求,它首先將這個請求委派給父類載入器去完成,每一個層次類載入器都是如此,則所有的類載入請求都會傳送到頂層的啟動類載入器,只有父載入器無法完成這個載入請求(即它的搜尋範圍中沒有找到所要的類),子類才嘗試載入。

使用雙親委派模型主要是兩個原因:

  • 可以避免重複載入,當父類已經載入了,則就子類不需再次載入;
  • 安全因素,如果不用這種,則使用者可以隨意的自定義載入器來替代Java核心API,則就會帶來安全隱患。

下面是一個類載入器雙親委派模型,這裡各個類載入器並不是繼承關係,它們利用組合實現的父類與子類關係。


 
雙親委託模型

類載入的幾種方式

  • 命令列啟動應用時候由JVM初始化載入,載入含有main的主類。
  • 通過Class.forName("Hello")方法動態載入類,預設會執行初始化塊,這是因為Class.forName("Hello")其實就是Class.forName("Hello",true,CALLCLASS.getClassLoader()),第二個引數就是類載入過程中的連線操作。如果指定了ClassLoader,則不會執行初始化塊。
  • 通過ClassLoader.loadClass("Hello")方法動態載入類,不會執行初始化塊,因為loadClass方法有兩個引數,使用者只是用第一個引數,第二個引數預設為false,即不對該類進行解析則就不會初始化。

類載入例項

當在命令列下執行:java HelloWorld(HelloWorld是含有main方法的類的Class檔案)JVM會將HelloWorld.class載入到記憶體中,並在堆中形成一個Class的物件HelloWorld.class

基本的載入流程如下:

  • 尋找jre目錄,尋找jvm.dll,並初始化JVM
  • 產生一個Bootstrap Loader(啟動類載入器);
  • Bootstrap Loader,該載入器會載入它指定路徑下的Java核心API,並且再自動載入Extended Loader(標準擴充套件類載入器),Extended Loader會載入指定路徑下的擴充套件JavaAPI,並將其父Loader設為BootstrapLoader
  • Bootstrap Loader也會同時自動載入AppClass Loader(系統類載入器),並將其父Loader設為ExtendedLoader
  • 最後由AppClass Loader載入CLASSPATH目錄下定義的類,HelloWorld類

建立自己的類載入器

Java應用開發過程中,可能會需要建立應用自己的類載入器。典型的場景包括實現特定的Java位元組程式碼查詢方式、對位元組程式碼進行加密/解密以及實現同名Java類的隔離等。建立自己的類載入器並不是一件複雜的事情,只需要繼承自java.lang.ClassLoader類並覆寫對應的方法即可。 java.lang.ClassLoader中提供的方法有不少,下面介紹幾個建立類載入器時需要考慮的:

  • defineClass():這個方法用來完成從Java位元組碼的位元組陣列到java.lang.Class的轉換。這個方法是不能被覆寫的,一般是用原生程式碼來實現的。
  • findLoadedClass():這個方法用來根據名稱查詢已經載入過的Java類。一個類載入器不會重複載入同一名稱的類。
  • findClass():這個方法用來根據名稱查詢並載入Java類
  • loadClass():這個方法用來根據名稱載入Java類
  • resolveClass():這個方法用來連結一個Java類

這裡比較 容易混淆的是findClass()方法和loadClass()方法的作用。前面提到過,在Java類的連結過程中,會需要對Java類進行解析,而解析可能會導致當前Java類所引用的其它Java類被載入。在這個時候,JVM就是通過呼叫當前類的定義類載入器的loadClass()方法來載入其它類的。findClass()方法則是應用建立的類載入器的擴充套件點。應用自己的類載入器應該覆寫findClass()方法來新增自定義的類載入邏輯。 loadClass()方法的預設實現會負責呼叫findClass()方法
前面提到,類載入器的代理模式預設使用的是父類優先的策略。這個策略的實現是封裝在loadClass()方法中的。如果希望修改此策略,就需要覆寫loadClass()方法

下面的程式碼給出了自定義的類載入的常見實現模式:

public class MyClassLoader extends ClassLoader {   
   protected Class<?> findClass(String name) throws ClassNotFoundException {       
      byte[] b = null; //查詢或生成Java類的位元組程式碼       
      return defineClass(name, b, 0, b.length);   
   }
}

Java垃圾回收機制

Java堆記憶體

分代收集

新生代(Young Generation)

  • Eden空間(Eden space,任何例項都通過Eden空間進入執行時記憶體區域)
  • S0 Survivor空間(S0 Survivor space,存在時間長的例項將會從Eden空間移動到S0 Survivor空間)
  • S1 Survivor空間 (存在時間更長的例項將會從S0 Survivor空間移動到S1 Survivor空間)

老年代(Old Generation)例項將從S1提升到Tenured(終身代)
永久代(Permanent Generation)包含類、方法等細節的元資訊

 
enter image description here

永久代空間在Java SE8特性中已經被移除。

垃圾回收過程

 
enter image description here

年輕代:使用標記複製清理演算法,解決記憶體碎片問題。因為在年輕代會有大量的記憶體需要回收,GC比較頻繁。通過這種方式來處理記憶體碎片化,然後在老年代中通過標記清理演算法來回收記憶體,因為在老年代需要被回收的記憶體比較少,提高效率。
Eden 區:當一個例項被建立了,首先會被儲存在堆記憶體年輕代的 Eden 區中。

Survivor 區(S0 和 S1):作為年輕代 GC(Minor GC)週期的一部分,存活的物件(仍然被引用的)從 Eden區被移動到 Survivor 區的 S0 中。類似的,垃圾回收器會掃描 S0 然後將存活的例項移動到 S1 中。總會有一個空的survivor區

老年代: 老年代(Old or tenured generation)是堆記憶體中的第二塊邏輯區。當垃圾回收器執行 Minor GC 週期時(物件年齡計數器),在 S1 Survivor 區中的存活例項將會被晉升到老年代,而未被引用的物件被標記為回收。老年代是例項生命週期的最後階段。Major GC 掃描老年代的垃圾回收過程。如果例項不再被引用,那麼它們會被標記為回收,否則它們會繼續留在老年代中。

記憶體碎片:一旦例項從堆記憶體中被刪除,其位置就會變空並且可用於未來例項的分配。這些空出的空間將會使整個記憶體區域碎片化。為了例項的快速分配,需要進行碎片整理。基於垃圾回收器的不同選擇,回收的記憶體區域要麼被不停地被整理,要麼在一個單獨的GC程式中完成。

根可達性演算法

Java語言規範沒有明確地說明JVM使用哪種垃圾回收演算法,但是任何一種垃圾收集演算法一般要做2件基本的事情:

  • 發現無用資訊物件
  • 回收被無用物件佔用的記憶體空間,使該空間可被程式再次使用。

GC Roots
根集就是正在執行的Java程式可以訪問的引用變數的集合(包括區域性變數、引數、類變數)

GC Roots的物件包括

  • 虛擬機器棧中所引用的物件(本地變數表)
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中JNI引用的物件(Native物件)

**可達性演算法分析 **

通過一系列稱為”GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋所有走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時(從GC Roots到此物件不可達),則證明此物件是不可用的,應該被回收。

根搜尋演算法:計算可達性,如圖:

 
根搜尋演算法

垃圾回收演算法

引用計數法

引用計數法是唯一沒有使用根集(GC Roots)的垃圾回收的法,該演算法使用引用計數器來區分存活物件和不再使用的物件。堆中的每個物件對應一個引用計數器。當每一次建立一個物件並賦給一個變數時,引用計數器置為1。當物件被賦給任意變數時,引用計數器每次加1,當物件出了作用域後(該物件丟棄不再使用),引用計數器減1,一旦引用計數器為0,物件就滿足了垃圾收集的條件。
唯一沒有使用根可達性演算法的垃圾回收演算法。
缺陷:不能解決迴圈引用的回收。

tracing演算法(tracing collector)

tracing演算法是為了解決引用計數法的問題而提出,它使用了根集(GC Roots)概念。垃圾收集器從根集開始掃描,識別出哪些物件可達,哪些物件不可達,並用某種方式標記可達物件,例如對每個可達物件設定一個或多個位。在掃描識別過程中,基於tracing演算法的垃圾收集也稱為標記和清除(mark-and-sweep)垃圾收集器

compacting演算法(Compacting Collector)

為了解決堆碎片問題,在清除的過程中,演算法將所有的物件移到堆的一端,堆的另一端就變成了一個相鄰的空閒記憶體區,收集器會對它移動的所有物件的所有引用進行更新,使得這些引用在新的位置能識別原來的物件。在基於Compacting演算法的收集器的實現中,一般增加控制程式碼和控制程式碼表。

copying演算法(Coping Collector)

該演算法的提出是為了克服控制程式碼的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個物件面和多個空閒面,程式從物件面為物件分配空間,當物件滿了,基於coping演算法的垃圾收集就從根集中掃描活動物件,並將每個活動物件複製到空閒面(使得活動物件所佔的記憶體之間沒有空閒洞),這樣空閒面變成了物件面,原來的物件面變成了空閒面,程式會在新的物件面中分配記憶體。

generation演算法(Generational Collector) :現在的java記憶體分割槽

stop-and-copy垃圾收集器的一個缺陷是收集器必須複製所有的活動物件,這增加了程式等待時間,這是coping演算法低效的原因。在程式設計中有這樣的規律:多數物件存在的時間比較短,少數的存在時間比較長。因此,generation演算法將堆分成兩個或多個,每個子堆作為物件的一代 (generation)。由於多數物件存在的時間比較短,隨著程式丟棄不使用的物件,垃圾收集器將從最年輕的子堆中收集這些物件。在分代式的垃圾收集器執行後,上次執行存活下來的物件移到下一最高代的子堆中,由於老一代的子堆不會經常被回收,因而節省了時間。

adaptive演算法(Adaptive Collector)

在特定的情況下,一些垃圾收集演算法會優於其它演算法。基於Adaptive演算法的垃圾收集器就是監控當前堆的使用情況,並將選擇適當演算法的垃圾收集器

 

相關文章