JVM體系結(jié)構(gòu)
JVM是一種解釋執(zhí)行class文件的規(guī)范技術(shù)。

我翻譯的中文圖:

類裝載器子系統(tǒng)
在JVM中負責裝載
.class文件(一種8位二進制流文件,各個數(shù)據(jù)項按順序緊密的從前向后排列, 相鄰的項之間沒有間隙,經(jīng)編譯器編譯.java源文件后生成,每個類(或者接口)都單獨占有一個class文件)。
運行時數(shù)據(jù)區(qū)
方法區(qū)
當
JVM使用類裝載器定位class文件,并將其輸入到內(nèi)存中時。會提取class文件的類型信息,并將這些信息存儲到方法區(qū)中。同時放入方法區(qū)中的還有該類型中的類靜態(tài)變量。
- 該類型的全限定名。如
java.io.FileOutputStream - 該類型的直接超類的全限定名。如
java.io.OutputStream - 該類型是類類型還是接口類型。
- 該類型的訪問修飾符(
public、abstract、final)。 - 任何直接超接口的全限定名的有序列表。如
java.io.Closeable,java.io.Flushable。 - 該類型的常量池。比如所有類型(Class)、方法、字段的符號、基本數(shù)據(jù)類型的直接數(shù)值(final)等。
- 字段信息:對類型中聲明的每個字段。
- 方法信息。
- 類靜態(tài)變量:靜態(tài)變量而不是放在堆里面,所以靜態(tài)屬于類,不屬于對象。
- 指向
ClassLoader類的引用。 - 指向
Class類的引用。 -
方法表:為了能快速定位到類型中的某個方法,
JVM對每個裝載的類型都會建立一個方法表,用于存儲該類型對象可以調(diào)用的方法的直接引用,這些方法就包括從超類中繼承來的。而這張表與Java動態(tài)綁定機制的實現(xiàn)是密切相關的。
常量池
常量池指的是在編譯期被確定,并被保存在已編譯的
.class文件中的一些數(shù)據(jù)。除了包含代碼中所定義的各種基本數(shù)據(jù)類型和對象型(String及數(shù)組)的常量值(final,在編譯時確定,并且編譯器會優(yōu)化)還包含一些以文本形式出現(xiàn)的符號引用(類信息),比如:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法和名稱和描述符
虛擬機必須給每個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序集合,包括直接常量(string、integer等)和其他類型,字段和方法的符號引用。
方法區(qū)是多線程共享的。也就是當虛擬機實例開始運行程序時,邊運行邊加載進class文件。不同的Class文件都會提取出不同類型信息存放在方法區(qū)中。同樣,方法區(qū)中不再需要運行的類型信息會被垃圾回收線程丟棄掉。
堆內(nèi)存
Java程序在運行時創(chuàng)建的所有類型對象和數(shù)組都存儲在堆中。JVM會根據(jù)new指令在堆中開辟一個確定類型的對象內(nèi)存空間。但是堆中開辟對象的空間并沒有任何人工指令可以回收,而是通過JVM的垃圾回收器負責回收。
- 堆中對象存儲的是該對象以及對象所有超類的實例數(shù)據(jù)(但不是靜態(tài)數(shù)據(jù))。
- 其中一個對象的引用可能在整個運行時數(shù)據(jù)區(qū)中的很多地方存在,比如Java棧,堆,方法區(qū)等。
- 堆中對象還應該關聯(lián)一個對象的鎖數(shù)據(jù)信息以及線程的等待集合(線程等待池)。這些都是實現(xiàn)Java線程同步機制的基礎。
- java中數(shù)組也是對象,那么自然在堆中會存儲數(shù)組的信息。
程序計數(shù)器
對于一個運行的Java而言,每一個線程都有一個PC寄存器。當線程執(zhí)行Java程序時,PC寄存器的內(nèi)容總是下一條將被執(zhí)行的指令地址。
Java棧
每啟動一個
線程,JVM都會為它分配一個Java棧,用于存放方法中的局部變量,操作數(shù)以及異常數(shù)據(jù)等。當線程調(diào)用某個方法時,JVM會根據(jù)方法區(qū)中該方法的字節(jié)碼組建一個棧幀。并將該棧幀壓入Java棧中,方法執(zhí)行完畢時,JVM會彈出該棧幀并釋放掉。
注意:Java棧中的數(shù)據(jù)是線程私有的,一個線程是無法訪問另一個線程的Java棧的數(shù)據(jù)。這也就是為什么多線程編程時,兩個相同線程執(zhí)行同一方法時,對方法內(nèi)的局部變量是不需要數(shù)據(jù)同步的原因。
成員變量有默認值(被final修飾且沒有static的必須顯式賦值),局部變量不會自動賦值。
執(zhí)行引擎
運行
Java的每一個線程都是一個獨立的虛擬機執(zhí)行引擎的實例。從線程生命周期的開始到結(jié)束,他要么在執(zhí)行字節(jié)碼,要么在執(zhí)行本地方法。一個線程可能通過解釋或者使用芯片級指令直接執(zhí)行字節(jié)碼,或者間接通過JIT(即時編譯器)執(zhí)行編譯過的本地代碼。
注意:JVM是進程級別,執(zhí)行引擎是線程級別。
指令集
實際上,
class文件中方法的字節(jié)碼流就是有JVM的指令序列構(gòu)成的。每一條指令包含一個單字節(jié)的操作碼,后面跟隨0個或多個操作數(shù)。
指令由一個操作碼和零個或多個操作數(shù)組成。
iload_0 // 把存儲在局部變量區(qū)中索引為0的整數(shù)壓入操作數(shù)棧。
iload_1 // 把存儲在局部變量區(qū)中索引為1的整數(shù)壓入操作數(shù)棧。
iadd // 從操作數(shù)棧中彈出兩個整數(shù)相加,在將結(jié)果壓入操作數(shù)棧。
istore_2 // 從操作數(shù)棧中彈出結(jié)果
很顯然,上面的指令反復用到了Java棧中的某一個方法棧幀。實際上執(zhí)行引擎運行Java字節(jié)碼指令很多時候都是在不停的操作Java棧,也有的時候需要在堆中開辟對象以及運行系統(tǒng)的本地指令等。但是Java棧的操作要比堆中的操作要快的多,因此反復開辟對象是非常耗時的。這也是為什么Java程序優(yōu)化的時候,盡量減少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會創(chuàng)建一個JVM實例(進行必要的初始化工作,比如:初始啟動類裝載器,初始運行時內(nèi)存數(shù)據(jù)區(qū)等。然后通過自定義類裝載器加載
Test.class。并提取Test.class字節(jié)碼中的信息存放在方法區(qū) 中(具體的信息在上面已經(jīng)講過)。上圖展示了方法區(qū)中的Test類信息,其中在常量池中有一個符號引用“Act”(類的全限定名,注意:這個引用目前還沒有真正的類信息的內(nèi)存地址)。接著
JVM開始從Test類的main字節(jié)碼處開始解釋執(zhí)行。在運行之前,會在Java棧中組建一個main方法的棧幀 ,如上圖Java棧所示。JVM需要運行任何方法前,通過在Java棧中壓入一個幀棧。在這個幀棧的內(nèi)存區(qū)域中進行計算。現(xiàn)在可以開始執(zhí)行
main方法的第一條指令 ——JVM需要為常量池的第一項的類(符號引用Act)分配內(nèi)存空間。但是Act類此時還沒有加載進JVM(因為常量池目前只有一個“Act”的符號引用)。JVM加載進Act.class,并提取Act類信息放入方法區(qū)中。然后以一個直接指向方法區(qū)Act類信息的直接引用(在棧中)換開始在常量池中的符號引用“Act”,這個過程就是常量池解析。以后就可以直接訪問Act的類信息了。此時
JVM可以根據(jù)方法區(qū)中的Act類信息,在堆中開辟一個Act類對象act。接著開始執(zhí)行
main方法中的第二條指令調(diào)用doMathForever方法。這個可以通過堆中act對象所指的方法表中查找,然后定位到方法區(qū)中的Act類信息中的doMathForever方法字節(jié)碼。在運行之前,仍然要組建一個doMathForever棧幀壓入Java棧。(注意:JVM會根據(jù)方法區(qū)中doMathForever的字節(jié)碼來創(chuàng)建棧幀的局部變量區(qū)和操作數(shù)棧的大小)接下來
JVM開始解釋運行Act.doMathForever字節(jié)碼的內(nèi)容了。
編譯和運行過程
編譯:源碼要運行,必須先轉(zhuǎn)成二進制的機器碼。這是編譯器的任務。
- 源文件由編譯器編譯成字節(jié)碼。 創(chuàng)建完源文件之后,程序會先被編譯為
.class文件。Java編譯一個類時,如果這個類所依賴的類還沒有被編譯,編譯器就會先編譯這個被依賴的類,然后引用,否則直接引用。如果java編譯器在指定目錄下找不到該類所其依賴的類的.class文件或者.java源文件的話,編譯器話報“cant find symbol”的錯誤。 - 編譯后的字節(jié)碼文件格式主要分為兩部分:常量池和方法字節(jié)碼。常量池記錄的是代碼出現(xiàn)過的所有
token(類名,成員變量名等等)以及符號引用(方法引用,成員變量引用等等);方法字節(jié)碼放的是類中各個方法的字節(jié)碼。
運行:
java類運行的過程大概可分為兩個過程:類的加載,類的執(zhí)行。需要說明的是:JVM主要在程序第一次主動使用類的時候,才會去加載該類。也就是說,JVM并不是在一開始就把一個程序就所有的類都加載到內(nèi)存中,而是到不得不用的時候才把它加載進來,而且只加載一次。
下面是程序運行的詳細步驟:
//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。系統(tǒng)就會啟動一個jvm進程,jvm進程從classpath路徑中找到一個名為AppMain.class的二進制文件,將MainApp的類信息加載到運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),這個過程叫做MainApp類的加載。 - 然后
JVM找到AppMain的主函數(shù)入口,開始執(zhí)行main函數(shù)。 -
main函數(shù)的第一條命令是Animal animal = new Animal("Puppy");就是讓JVM創(chuàng)建一個Animal對象,但是這時候方法區(qū)中沒有Animal類的信息,所以JVM馬上加載Animal類,把Animal類的類型信息放到方法區(qū)中。 - 加載完
Animal類之后,Java虛擬機做的第一件事情就是在堆區(qū)中為一個新的Animal實例分配內(nèi)存,然后調(diào)用構(gòu)造函數(shù)初始化Animal實例,這個Animal實例持有著指向方法區(qū)的Animal類的類型信息(其中包含有方法表,java動態(tài)綁定的底層實現(xiàn))的引用。 - 當使用
animal.printName()的時候,JVM根據(jù)animal引用找到Animal對象,然后根據(jù)Animal對象持有的引用定位到方法區(qū)中Animal類的類型信息的方法表,獲得printName()函數(shù)的字節(jié)碼的地址。 - 開始運行
printName()函數(shù)的字節(jié)碼(可以把字節(jié)碼理解為一條條的指令)。
圖示
特別說明:java類中所有public和protected的實例方法都采用動態(tài)綁定機制,所有私有方法、靜態(tài)方法、構(gòu)造器及初始化方法<clinit>都是采用靜態(tài)綁定機制。而使用動態(tài)綁定機制的時候會用到方法表,靜態(tài)綁定時并不會用到。
通過前面的兩個例子的分析,應該理解了不少了吧。
類加載機制
JVM主要包含三大核心部分:類加載器,運行時數(shù)據(jù)區(qū)和執(zhí)行引擎。
虛擬機將描述類的數(shù)據(jù)從class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗,準備,解析和初始化,最終就會形成可以被虛擬機使用的java類型,這就是一個虛擬機的類加載機制。java在類中的類是動態(tài)加載的,只有在運行期間使用到該類的時候,才會將該類加載到內(nèi)存中,java依賴于運行期動態(tài)加載和動態(tài)鏈接來實現(xiàn)類的動態(tài)使用。
一個類的生命周期:

加載,驗證,準備,初始化和卸載在開始的順序上是固定的,但是可以交叉進行。
在Java中,對于類有且僅有四種情況會對類進行“初始化”。
- 使用
new關鍵字實例化對象的時候,讀取或設置一個類的靜態(tài)字段時候(除final修飾的static外),調(diào)用類的靜態(tài)方法時候,都只會初始化該靜態(tài)字段或者靜態(tài)方法所定義的類。 - 使用
reflect包對類進行反射調(diào)用的時候,如果類沒有進行初始化,則先要初始化該類。 - 當初始化一個類的時候,如果其父類沒有初始化過,則先要觸發(fā)其父類初始化。
- 虛擬機啟動的時候,會初始化一個有
main方法的主類。
注意:
- 子類引用父類靜態(tài)字段,只會初始化父類不會初始化子類
- 通過數(shù)組定義來引用類,也不會觸發(fā)該類的初始化
- 常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上沒有直接引用到定義常量的類,因此也不會觸發(fā)定義常量的類的初始化
類加載過程
加載
加載階段主要完成三件事,即通過一個類的全限定名來獲取定義此類的二進制字節(jié)流,將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu),在
Java堆中生成一個代表此類的Class對象,作為訪問方法區(qū)這些數(shù)據(jù)的入口。這個加載過程主要就是靠類加載器實現(xiàn)的,這個過程可以由用戶自定義類的加載過程。
驗證
這個階段目的在于確保才class文件的字節(jié)流中包含信息符合當前虛擬機要求,不會危害虛擬機自身安全。
主要包括四種驗證:
-
文件格式驗證:基于字節(jié)流驗證,驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當前虛擬機處理。 -
元數(shù)據(jù)驗證:基于方法區(qū)的存儲結(jié)構(gòu)驗證,對字節(jié)碼描述信息進行語義驗證。 -
字節(jié)碼驗證:基于方法區(qū)的存儲結(jié)構(gòu)驗證,進行數(shù)據(jù)流和控制流的驗證。 -
符號引用驗證:基于方法區(qū)的存儲結(jié)構(gòu)驗證,發(fā)生在解析中,是否可以將符號引用成功解析為直接引用。
準備
僅僅為類變量(即static修飾的字段變量)分配內(nèi)存并且設置該類變量的初始值即零值,這里不包含用final修飾的static,因為final在編譯的時候就會分配了(編譯器的優(yōu)化),同時這里也不會為實例變量分配初始化。類變量會分配在方法區(qū)中,而實例變量是會隨著對象一起分配到Java堆中。
解析
解析主要就是將常量池中的符號引用替換為直接引用的過程。符號引用就是一組符號來描述目標,可以是任何字面量,而直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。有類或接口的解析,字段解析,類方法解析,接口方法解析。
初始化
初始化階段依舊是初始化類變量和其他資源,這里將執(zhí)行用戶的static字段和靜態(tài)語句塊的賦值操作。這個過程就是執(zhí)行類構(gòu)造器< clinit >方法的過程。
< clinit >方法是由編譯器收集類中所有類變量的賦值動作和靜態(tài)語句塊的語句生成的,類構(gòu)造器< clinit >方法與實例構(gòu)造器< init >方法不同,這里面不用顯示的調(diào)用父類的< clinit >方法,父類的< clinit >方法會自動先執(zhí)行于子類的< clinit >方法。即父類定義的靜態(tài)語句塊和靜態(tài)字段都要優(yōu)先子類的變量賦值操作。
類加載器
類加載器的分類
-
啟動類加載器(Bootstrap ClassLoader):主要負責加載<JAVA_HOME>\lib目錄中的'.'或是-Xbootclasspath參數(shù)指定的路徑中的,并且可以被虛擬機識別(僅僅按照文件名識別的)的類庫到虛擬機內(nèi)存中。它加載的是System.getProperty("sun.boot.class.path")所指定的路徑或jar。 -
擴展類加載器(Extension ClassLoader):主要負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫。它加載的是
System.getProperty("java.ext.dirs")所指定的路徑或jar。 -
應用程序類加載器(Application ClassLoader):也叫系統(tǒng)類加載器,主要負責加載ClassPath路徑上的類庫,如果應用程序沒有自定義自己類加載器,則這個就是默認的類加載器。它加載的是System.getProperty("java.class.path")所指定的路徑或jar。
類加載器的特點
- 運行一個程序時,總是由
Application Loader(系統(tǒng)類加載器)開始加載指定的類。 - 在加載類時,每個類加載器會將加載任務上交給其父,如果其父找不到,再由自己去加載。
-
Bootstrap Loader(啟動類加載器)是最頂級的類加載器了,其父加載器為null。
類加載器的雙親委派模型
類加載器雙親委派模型的工作過程是:如果一個類加載器收到一個類加載的請求,它首先將這個請求委派給父類加載器去完成,每一個層次類加載器都是如此,則所有的類加載請求都會傳送到頂層的啟動類加載器,只有父加載器無法完成這個加載請求(即它的搜索范圍中沒有找到所要的類),子類才嘗試加載。
使用雙親委派模型主要是兩個原因:
- 可以避免重復加載,當父類已經(jīng)加載了,則就子類不需再次加載;
- 安全因素,如果不用這種,則用戶可以隨意的自定義加載器來替代
Java核心API,則就會帶來安全隱患。
下面是一個類加載器雙親委派模型,這里各個類加載器并不是繼承關系,它們利用組合實現(xiàn)的父類與子類關系。

類加載的幾種方式
- 命令行啟動應用時候由
JVM初始化加載,加載含有main的主類。 - 通過
Class.forName("Hello")方法動態(tài)加載類,默認會執(zhí)行初始化塊,這是因為Class.forName("Hello")其實就是Class.forName("Hello",true,CALLCLASS.getClassLoader()),第二個參數(shù)就是類加載過程中的連接操作。如果指定了ClassLoader,則不會執(zhí)行初始化塊。 - 通過
ClassLoader.loadClass("Hello")方法動態(tài)加載類,不會執(zhí)行初始化塊,因為loadClass方法有兩個參數(shù),用戶只是用第一個參數(shù),第二個參數(shù)默認為false,即不對該類進行解析則就不會初始化。
類加載實例
當在命令行下執(zhí)行:java HelloWorld(HelloWorld是含有main方法的類的Class文件),JVM會將HelloWorld.class加載到內(nèi)存中,并在堆中形成一個Class的對象HelloWorld.class。
基本的加載流程如下:
- 尋找
jre目錄,尋找jvm.dll,并初始化JVM; - 產(chǎn)生一個
Bootstrap Loader(啟動類加載器); -
Bootstrap Loader,該加載器會加載它指定路徑下的Java核心API,并且再自動加載Extended Loader(標準擴展類加載器),Extended Loader會加載指定路徑下的擴展JavaAPI,并將其父Loader設為BootstrapLoader。 -
Bootstrap Loader也會同時自動加載AppClass Loader(系統(tǒng)類加載器),并將其父Loader設為ExtendedLoader。 - 最后由
AppClass Loader加載CLASSPATH目錄下定義的類,HelloWorld類。
創(chuàng)建自己的類加載器
在Java應用開發(fā)過程中,可能會需要創(chuàng)建應用自己的類加載器。典型的場景包括實現(xiàn)特定的Java字節(jié)代碼查找方式、對字節(jié)代碼進行加密/解密以及實現(xiàn)同名Java類的隔離等。創(chuàng)建自己的類加載器并不是一件復雜的事情,只需要繼承自java.lang.ClassLoader類并覆寫對應的方法即可。 java.lang.ClassLoader中提供的方法有不少,下面介紹幾個創(chuàng)建類加載器時需要考慮的:
-
defineClass():這個方法用來完成從Java字節(jié)碼的字節(jié)數(shù)組到java.lang.Class的轉(zhuǎn)換。這個方法是不能被覆寫的,一般是用原生代碼來實現(xiàn)的。 -
findLoadedClass():這個方法用來根據(jù)名稱查找已經(jīng)加載過的Java類。一個類加載器不會重復加載同一名稱的類。 -
findClass():這個方法用來根據(jù)名稱查找并加載Java類。 -
loadClass():這個方法用來根據(jù)名稱加載Java類。 -
resolveClass():這個方法用來鏈接一個Java類。
這里比較 容易混淆的是findClass()方法和loadClass()方法的作用。前面提到過,在Java類的鏈接過程中,會需要對Java類進行解析,而解析可能會導致當前Java類所引用的其它Java類被加載。在這個時候,JVM就是通過調(diào)用當前類的定義類加載器的loadClass()方法來加載其它類的。findClass()方法則是應用創(chuàng)建的類加載器的擴展點。應用自己的類加載器應該覆寫findClass()方法來添加自定義的類加載邏輯。 loadClass()方法的默認實現(xiàn)會負責調(diào)用findClass()方法。
前面提到,類加載器的代理模式默認使用的是父類優(yōu)先的策略。這個策略的實現(xiàn)是封裝在loadClass()方法中的。如果希望修改此策略,就需要覆寫loadClass()方法。
下面的代碼給出了自定義的類加載的常見實現(xiàn)模式:
public class MyClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = null; //查找或生成Java類的字節(jié)代碼
return defineClass(name, b, 0, b.length);
}
}
Java垃圾回收機制
Java堆內(nèi)存
分代收集
新生代(Young Generation)
-
Eden空間(Eden space,任何實例都通過Eden空間進入運行時內(nèi)存區(qū)域) -
S0 Survivor空間(S0 Survivor space,存在時間長的實例將會從Eden空間移動到S0 Survivor空間) -
S1 Survivor空間 (存在時間更長的實例將會從S0 Survivor空間移動到S1 Survivor空間)
老年代(Old Generation)實例將從S1提升到Tenured(終身代)
永久代(Permanent Generation)包含類、方法等細節(jié)的元信息

永久代空間在Java SE8特性中已經(jīng)被移除。
垃圾回收過程

年輕代:使用標記復制清理算法,解決內(nèi)存碎片問題。因為在年輕代會有大量的內(nèi)存需要回收,GC比較頻繁。通過這種方式來處理內(nèi)存碎片化,然后在老年代中通過標記清理算法來回收內(nèi)存,因為在老年代需要被回收的內(nèi)存比較少,提高效率。
Eden 區(qū):當一個實例被創(chuàng)建了,首先會被存儲在堆內(nèi)存年輕代的 Eden 區(qū)中。
Survivor 區(qū)(S0 和 S1):作為年輕代 GC(Minor GC)周期的一部分,存活的對象(仍然被引用的)從 Eden區(qū)被移動到 Survivor 區(qū)的 S0 中。類似的,垃圾回收器會掃描 S0 然后將存活的實例移動到 S1 中??倳幸粋€空的survivor區(qū)。
老年代: 老年代(Old or tenured generation)是堆內(nèi)存中的第二塊邏輯區(qū)。當垃圾回收器執(zhí)行 Minor GC 周期時(對象年齡計數(shù)器),在 S1 Survivor 區(qū)中的存活實例將會被晉升到老年代,而未被引用的對象被標記為回收。老年代是實例生命周期的最后階段。Major GC 掃描老年代的垃圾回收過程。如果實例不再被引用,那么它們會被標記為回收,否則它們會繼續(xù)留在老年代中。
內(nèi)存碎片:一旦實例從堆內(nèi)存中被刪除,其位置就會變空并且可用于未來實例的分配。這些空出的空間將會使整個內(nèi)存區(qū)域碎片化。為了實例的快速分配,需要進行碎片整理?;诶厥掌鞯牟煌x擇,回收的內(nèi)存區(qū)域要么被不停地被整理,要么在一個單獨的GC進程中完成。
根可達性算法
Java語言規(guī)范沒有明確地說明JVM使用哪種垃圾回收算法,但是任何一種垃圾收集算法一般要做2件基本的事情:
- 發(fā)現(xiàn)無用信息對象
- 回收被無用對象占用的內(nèi)存空間,使該空間可被程序再次使用。
GC Roots
根集就是正在執(zhí)行的Java程序可以訪問的引用變量的集合(包括局部變量、參數(shù)、類變量)
GC Roots的對象包括
- 虛擬機棧中所引用的對象(本地變量表)
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI引用的對象(Native對象)
**可達性算法分析 **
通過一系列稱為”GC Roots”的對象作為起點,從這些節(jié)點開始向下搜索,搜索所有走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時(從GC Roots到此對象不可達),則證明此對象是不可用的,應該被回收。
根搜索算法:計算可達性,如圖:
垃圾回收算法
引用計數(shù)法
引用計數(shù)法是唯一沒有使用根集(GC Roots)的垃圾回收的法,該算法使用引用計數(shù)器來區(qū)分存活對象和不再使用的對象。堆中的每個對象對應一個引用計數(shù)器。當每一次創(chuàng)建一個對象并賦給一個變量時,引用計數(shù)器置為1。當對象被賦給任意變量時,引用計數(shù)器每次加1,當對象出了作用域后(該對象丟棄不再使用),引用計數(shù)器減1,一旦引用計數(shù)器為0,對象就滿足了垃圾收集的條件。
唯一沒有使用根可達性算法的垃圾回收算法。
缺陷:不能解決循環(huán)引用的回收。
tracing算法(tracing collector)
tracing算法是為了解決引用計數(shù)法的問題而提出,它使用了根集(GC Roots)概念。垃圾收集器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,并用某種方式標記可達對象,例如對每個可達對象設置一個或多個位。在掃描識別過程中,基于tracing算法的垃圾收集也稱為標記和清除(mark-and-sweep)垃圾收集器。
compacting算法(Compacting Collector)
為了
解決堆碎片問題,在清除的過程中,算法將所有的對象移到堆的一端,堆的另一端就變成了一個相鄰的空閑內(nèi)存區(qū),收集器會對它移動的所有對象的所有引用進行更新,使得這些引用在新的位置能識別原來的對象。在基于Compacting算法的收集器的實現(xiàn)中,一般增加句柄和句柄表。
copying算法(Coping Collector)
該算法的提出是為了克服
句柄的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個對象面和多個空閑面,程序從對象面為對象分配空間,當對象滿了,基于coping算法的垃圾收集就從根集中掃描活動對象,并將每個活動對象復制到空閑面(使得活動對象所占的內(nèi)存之間沒有空閑洞),這樣空閑面變成了對象面,原來的對象面變成了空閑面,程序會在新的對象面中分配內(nèi)存。
generation算法(Generational Collector) :現(xiàn)在的java內(nèi)存分區(qū)
stop-and-copy垃圾收集器的一個缺陷是收集器必須復制所有的活動對象,這增加了程序等待時間,這是coping算法低效的原因。在程序設計中有這樣的規(guī)律:多數(shù)對象存在的時間比較短,少數(shù)的存在時間比較長。因此,generation算法將堆分成兩個或多個,每個子堆作為對象的一代 (generation)。由于多數(shù)對象存在的時間比較短,隨著程序丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行后,上次運行存活下來的對象移到下一最高代的子堆中,由于老一代的子堆不會經(jīng)常被回收,因而節(jié)省了時間。
adaptive算法(Adaptive Collector)
在特定的情況下,一些垃圾收集算法會優(yōu)于其它算法?;贏daptive算法的垃圾收集器就是監(jiān)控當前堆的使用情況,并將選擇適當算法的垃圾收集器。
