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

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

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等)和其餘類型,字段和方法的符號引用jvm

方法區是多線程共享的。也就是當虛擬機實例開始運行程序時,邊運行邊加載進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算法的垃圾收集器就是監控當前堆的使用狀況,並將選擇適當算法的垃圾收集器

相關文章
相關標籤/搜索