Java虛擬機03--垃圾算法與內(nèi)存分配策略

概述

垃圾回收(Garbage Collection,GC):其歷史比Java久遠。1960年誕生于MIT的Lisp是第一門真正使用內(nèi)存動態(tài)分
配和垃圾收集技術(shù)的語言。

GC需要完成的3件事
  • 哪些內(nèi)存需要回收
  • 什么時候進行回收
  • 如何回收

    之前介紹了java內(nèi)存運行時區(qū)域的各個部分,其中程序計數(shù)器、虛擬機棧、本地方法棧三個區(qū)域隨線程而生,隨線程滅亡;棧中的棧幀隨著方法的進入和退出而有條不紊地執(zhí)行著出棧和入棧操作。每一個棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時就已知的(盡管在運行期會由JIT編譯器進行一些優(yōu)化,大體上可以認為是編譯期可知的)。因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個區(qū)域內(nèi)就不需要過多考慮回收的問題,因為方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟隨著回收了。而Java堆和方法區(qū)則不一樣,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運行期間時才能知道會創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收都是動態(tài)的,垃圾收集器所關(guān)注的是這部分內(nèi)存

對象已死?

在堆中存放著各種各樣的對象實例,在垃圾回收之前,第一件事就是確定這些對象那些是有用的,哪些是不可能在被任何途徑使用的對象

引用計數(shù)算法

定義:給一個對象中添加一個引用計數(shù)器,每當有一個地方引用他的是偶,計數(shù)器的值進行+1操作;當引用失效時,計數(shù)器值就減一;任何時刻計數(shù)器為0的對象是不可能再本被使用的。
客觀的說,引用計數(shù)算法(Reference Counting)的實現(xiàn)簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法。但是,在J主流的Java虛擬機當中并沒有選用引用技術(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對象之間的相互循環(huán)引用問題

舉個簡單的例子

/**
*testGC()方法執(zhí)行后,objA和objB會不會被GC呢?
*@author zzm
*/
public class ReferenceCountingGC{
public Object instance=null;
private static final int_1MB=1024*1024;
/**
*這個成員屬性的唯一意義就是占點內(nèi)存,以便能在GC日志中看清楚是否被回收過
*/
      private byte[]bigSize=new byte[2*_1MB];
      public static void testGC(){
      ReferenceCountingGC objA=new ReferenceCountingGC();
      ReferenceCountingGC objB=new ReferenceCountingGC();
      objA.instance=objB;
      objB.instance=objA;
      objA=null;
      objB=null;
      //假設在這行發(fā)生GC,objA和objB是否能被回收?
      System.gc();
    }
}

代碼清單中的testGC()方法,對象objA和objB都有字段instance,賦值令objA.instance=objB及objB.instance=objA,除此之外,這兩個對象再無任何引用,實際上這兩個對象已經(jīng)不可能再被訪問(只是這兩者相互引用但是并沒有實際的作用,理應被回收),但是它們因為互相引用著對方,導致它們的引用計數(shù)都不為0,于是引用計數(shù)算法無法通知GC收集器回收它們。

運行結(jié)果

[F u l l G C(S y s t e m)[T e n u r e d:0 K->2 1 0 K(1 0 2 4 0 K),0.0 1 4 9 1 4 2 s e c s]4603K->210K(19456K),[Perm:2999K->
2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]
Heap
def new generation total 9216K,used 82K[0x00000000055e0000,0x0000000005fe0000,0x0000000005fe0000)
Eden space 8192K,1%used[0x00000000055e0000,0x00000000055f4850,0x0000000005de0000)
from space 1024K,0%used[0x0000000005de0000,0x0000000005de0000,0x0000000005ee0000)
to space 1024K,0%used[0x0000000005ee0000,0x0000000005ee0000,0x0000000005fe0000)
tenured generation total 10240K,used 210K[0x0000000005fe0000,0x00000000069e0000,0x00000000069e0000)
the space 10240K,2%used[0x0000000005fe0000,0x0000000006014a18,0x0000000006014c00,0x00000000069e0000)
compacting perm gen total 21248K,used 3016K[0x00000000069e0000,0x0000000007ea0000,0x000000000bde0000)
the space 21248K,14%used[0x00000000069e0000,0x0000000006cd2398,0x0000000006cd2400,0x0000000007ea0000)

從運行結(jié)果中可以清楚看到,GC日志中包含“4603K->210K" (進行了回收操作),意味著虛擬機并沒有因為這兩個對象互相引用就不回收它們,這也從側(cè)面說明虛擬機并不是通過引用計數(shù)算法來判斷對象是否存活的


可達性分析算法

算法的基本思想:通過一系列的稱為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。對象object 5、object 6、object 7雖然互相有關(guān)聯(lián),但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。簡單點理解就是與GC ROOT是否有關(guān).

可達性算法判定對象是否可回收

在Java語言中,可作為GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象(比如new了一個Object,并賦值給了一個局部變量,在這個變量沒有被銷毀之前,new的這個Object就會成為GC ROOT)
  • 方法區(qū)中類靜態(tài)屬性引用的對象
  • 方法區(qū)中常量引用的對象
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象
  • 活躍線程的引用對象

無論是通過引用計數(shù)算法判斷對象的引用數(shù)量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關(guān)。在JDK 1.2以前:如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個引用。不能在內(nèi)存足夠的情況下保留一下想要保留“非應用對象”。如果內(nèi)存空間在進行垃圾收集后還是非常緊張,則可以拋棄這些對象。

在JDK 1.2之后,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

  • 強引用就是指在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象.當內(nèi)存空間不足的時候,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止。
  • 軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。在JDK 1.2之后,提供了SoftReference類來實現(xiàn)軟引用。
  • 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當垃圾收集器工作時,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。在JDK 1.2之后,提供了WeakReference類來實現(xiàn)弱引用
  • 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回時收到一個系統(tǒng)通知。在JDK 1.2之后,提供了PhantomReference類來實現(xiàn)虛引用。

是否被回收

即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標記過程:如果對象在進行可達性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象(第一次篩選)是否有必要執(zhí)行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”。

如果這個對象被判定為有必要執(zhí)行finalize()方法,那么這個對象將會放置在一個叫做F-Queue的隊列之中,并在稍后由一個由虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行它。這里所謂的“執(zhí)行”是指虛擬機會觸發(fā)這個方法,但并不承諾會等待它運行結(jié)束,這樣做的原因是,如果一個對象在finalize()方法中執(zhí)行緩慢,或者發(fā)生了死循環(huán)(更極端的情
),將很可能會導致F-Queue隊列中其他對象永久處于等待,甚至導致整個內(nèi)存回收系統(tǒng)崩潰。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規(guī)模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可,譬如把自己(this關(guān)鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。

一次對象自我拯救的演示

/**
*此代碼演示了兩點:
*1.對象可以在被GC時自我拯救。
*2.這種自救的機會只有一次,因為一個對象的finalize()方法最多只會被系統(tǒng)自動調(diào)用一次
*@author zzm
*/
public class FinalizeEscapeGC{

       public static FinalizeEscapeGC SAVE_HOOK=null;

       public void isAlive(){
              System.out.println("yes,i am still alive:)");
       }
      @Override
       protected void finalize()throws Throwable{
             super.finalize();
             System.out.println("finalize mehtod executed!");
             //進行引用
             FinalizeEscapeGC.SAVE_HOOK=this;
         }
        public static void main(String[]args)throws Throwable{
             SAVE_HOOK=new FinalizeEscapeGC();
             //對象第一次成功拯救自己
             SAVE_HOOK=null;
             System.gc();
              //因為finalize方法優(yōu)先級很低,所以暫停0.5秒以等待它
             Thread.sleep(500);
              if(SAVE_HOOK!=null){
                    SAVE_HOOK.isAlive();
             }else{
                    System.out.println("no,i am dead:(");
             }
             //下面這段代碼與上面的完全相同,但是這次自救卻失敗了
             SAVE_HOOK=null;
             System.gc();
            //因為finalize方法優(yōu)先級很低,所以暫停0.5秒以等待它
            Thread.sleep(500);
            if(SAVE_HOOK!=null){
                   SAVE_HOOK.isAlive();
            }else{
              System.out.println("no,i am dead:(");
            }
    }
}

運行結(jié)果:

finalize mehtod executed!
yes,i am still alive:)
no,i am dead:(

從運行結(jié)果可以看出,SAVE_HOOK對象的finalize()方法確實被GC收集器觸發(fā)過,并且在被收集前成功逃脫了。

另外一個值得注意的地方是,代碼中有兩段完全一樣的代碼片段,執(zhí)行結(jié)果卻是一次逃脫成功,一次失敗,這是因為任何一個對象的finalize()方法都只會被系統(tǒng)自動調(diào)用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執(zhí)行,因此第二段代碼的自救行動失敗了.

上面關(guān)于對象死亡finalize()方法的描述可能帶有悲情的藝術(shù)色彩,但是建議大家盡量避免使用它.finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時.


回收方法區(qū)

很多人認為方法區(qū)(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的(實際上是有的),Java虛擬機規(guī)范中確實說過可以不要求虛擬機在方法區(qū)實現(xiàn)垃圾收集,而且在方法區(qū)中進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規(guī)應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低于此。

永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量無用的類?;厥諒U棄常量與回收Java堆中的對象非常類似。以常量池中字面量的回收為例,假如一個字符串“abc”已經(jīng)進入了常量池中,但是當前系統(tǒng)沒有任何一個String對象是叫做“abc”的,換句話說,就是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果這時發(fā)生內(nèi)存回收,而且必要的話,這個“abc”常量就會被系統(tǒng)清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

判定一個常量是否是"廢棄常量"比較簡單,而要判定一個類是否是"無用的類"條件則相對苛刻許多.
類需要同時滿足下面三個條件才算是"無用的類":

  • 該類的所有實例都已經(jīng)被回收,也就是Java堆中不存在該類中的任何實例
  • 加載該類的ClassLoader已經(jīng)被回收
  • 該類對應的java.lang.class對象沒有在任何的地方被引用,無法在任何的地方通過反射訪問該類的方法

虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數(shù)進行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版的虛擬機支持。

在大量使用反射,動態(tài)代理,GGLib等ByteCOde框架,動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。


垃圾搜集算法

標記-清除算法(碎片化)

最基礎的收集算法是“標記-清除”(Mark-Sweep)算法,如同它的名字一樣,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象,后續(xù)的收集算法都是基于這種思路并對其不足進行改進而得到的。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。

標記階段: 先通過根節(jié)點,標記所有從根節(jié)點開始的可達對象。因此,未被標記的對象就是未被引用的垃圾對象;
清除階段:對堆內(nèi)存從頭到尾進行線性遍歷,回收不可達對象

“標記-清除”算法

“標記-清除”算法示意圖

復制算法

  • 分為對象面和空閑面
  • 對象在對象面上創(chuàng)建
  • 存活的對象被從對象面復制到空閑面
  • 對象面的所有對象內(nèi)存清除

將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。

優(yōu)點

  • 這樣使得每次對整個辦區(qū)進行回收,內(nèi)存分配的時候就不用考慮內(nèi)存碎片等情況
  • 只要移動堆頂指針,按照順序分配內(nèi)存即可,運行效率高

缺點

  • 空間的浪費,賦值算法要想使用,最起碼對象的存活率要非常低才行

現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代,IBM公司的專門研究表明,新生代中的對象98%是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內(nèi)空間為整個新生代容量的90%(80%+10%),只有10%的內(nèi)存會被“浪費”。當然,98%的對象可回收只是一般場景下的據(jù),我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他內(nèi)存(這里指老年代)進行分配擔保(Handle Promotion)。

復制算法示意圖

復制算法在對象存活率高的時候要進行較多的復制操作,效率將會降低,所以在老年代中一般不能直接選用這種算法。

標記-整理算法

(在標記清除的基礎上進行了移動)

  • 標記:從根集合開始掃描,對存活對象進行標記
  • 清除:一共所有存活對象,且按照內(nèi)存地址依次排列,然后將末端內(nèi)存地址以后的內(nèi)存全部回收

標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存.

優(yōu)點

  • 不會產(chǎn)生內(nèi)存碎片(避免了內(nèi)存的不連續(xù))
  • 不用設置兩塊內(nèi)存互換
  • 適用于存活率高的場景

缺點

  • 在標記的基礎之上還需要進行對象的移動,成本相對較高,效率也不高


    “標記-整理”算法示意圖

分代收集算法

當前商業(yè)虛擬機的垃圾收集都采用“分代收集”(Generational Collection)算法,這種算法并沒有什么新的思想,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般是把java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴ā?/p>

按照對象生命周期的不同劃分區(qū)域采用不同的垃圾回收算法

  • 在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
  • 老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。

新生代

區(qū)域分配示意圖

From 區(qū)和 To區(qū)域是相對的,當From區(qū)的對象傳到To區(qū)域的時候Eden和SurvivorTo會被清空(第一次從Eden到Survivor因為沒有From區(qū),所以不會清空From[也可以理解為空白區(qū)域被清除]),每次對象在Survivor渡過一次GC,其年齡也會加一

當?shù)竭_一定的年齡的時候,默認是15,(可以通過 -XX:MaxTenuringThreshold參數(shù)設置)成為老年代,對默寫大對象也會直接進入老年代

對象如何晉升到老年代

  • 經(jīng)歷一定Minor次數(shù)之后依舊存活的對象
    • Minor GC觸發(fā)條件:當Eden區(qū)滿時,觸發(fā)Minor GC。
  • Survivor區(qū)中放不下的對象

老年代

FullGC比MinorGC慢,但執(zhí)行頻率低
觸發(fā)FullGC的條件:

  • 老年代空間不足
  • 永久代空間不足(JDK1.7以下)
  • CMS GC時出現(xiàn)promotion filed,Concurrent mode filed
  • Minor GC晉升到老年代的平均大小大于老年代的剩余空間
  • 調(diào)用System.gc()
  • 系統(tǒng)建議執(zhí)行Full GC,但是不必然執(zhí)行
  • 使用 RMI來進行RPC或管理的JDK應用,每小時執(zhí)行1次Full GC

HotSpot的算法實現(xiàn)

在HotSpot虛擬機上實現(xiàn)了對象存活判定算法和垃圾收集算法時,必須對算法的執(zhí)行效率有嚴格的考量,才能保證虛擬機高效運行。

枚舉根節(jié)點

在可達性分析中GC ROOTS節(jié)點找應用鏈這個操作為例,,可作為GC Roots的節(jié)點主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中,現(xiàn)在很多應用僅僅方法區(qū)就有數(shù)百兆,如果要逐個檢查這里面的引用,那么必然會消耗很多時間

另外,可達性分析對執(zhí)行時間的敏感還體現(xiàn)在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行——這里“一致性”的意思是指在整個分析期間整個執(zhí)行系統(tǒng)看起來就像被凍結(jié)在某個時間點上,不可以出現(xiàn)分析過程中對象引用關(guān)系還在不斷變化的情況,該點不滿足的話分析結(jié)果準確性就無法得到保證。這點是導致GC進行時必須停頓所有Java執(zhí)行線程(Sun將這件事情稱為"Stop The World")的其中一個重要原因,及時實在(幾乎)不會發(fā)生停頓的CMS收集器中,枚舉根節(jié)點時也是必須要停頓的.

目前的主流Java虛擬機使用的都是準確式GC(就是讓JVM知道內(nèi)存中某位置數(shù)據(jù)的類型什么),所以當執(zhí)行系統(tǒng)停頓下來后,并不需要一個不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機應當是有辦法直接得知哪些地方存放著對象引用。HotSpot的實現(xiàn)中,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達到這個目的的,在類加載完成的時候,HotSpot就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些信息了。

下面的代碼清單是HotSpot Client VM生成的一段String.hashCode()方法的本地代碼,可以看到在0x026eb7a9處的call指令有OopMap記錄,它指明了EBX寄存器和棧中偏移量為16的內(nèi)存區(qū)域中各有一個普通對象指針(Ordinary Object Pointer)的引用,有效范圍為從call指令開始直到0x026eb730(指令流的起始位置)+142(OopMap記錄的偏移量)=0x026eb7be,即hlt指令為止。

0x026eb730:mov%eax,-0x8000(%esp)
……;ImplicitNullCheckStub slow case
0x026eb7a9:call 0x026e83e0 ;OopMap{ebx=Oop[16]=Oop off=142} ;*caload ;-java.lang.String:hashCode@48(line 1489)
;{runtime_call}
0x026eb7ae:push$0x83c5c18 ;{external_word}
0x026eb7b3:call 0x026eb7b8
0x026eb7b8:pusha
0x026eb7b9:call 0x0822bec0;{runtime_call}
0x026eb7be:hlt

安全點(進行GC的位置)

在OopMap的協(xié)助下,HotSpot可以快速且準確地完成GC Roots枚舉,但一個很現(xiàn)實的問題隨之而來:可能導致引用關(guān)系變化,或者說OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高

實際上,HotSpot也的確沒有為每條指令都生成OopMap,前面已經(jīng)提到,只是在“特定的位置”記錄了這些信息,這些位置稱為安全點(Safepoint),即程序執(zhí)行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。

Safepoint的選定既不能太少以致于讓GC等待時間太長,也不能過于頻繁以致于過分增大運行時的負荷。所以,安全點的選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特征”為標準進行選定的——因為每條指令執(zhí)行的時間都非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行,“長時間執(zhí)行”的最明顯特征就是指令序列復用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以具有這些功能的指令才會產(chǎn)生Safepoint。

對于Sefepoint,另一個需要考慮的問題是如何在GC發(fā)生時讓所有線程(這里不包括執(zhí)行JNI調(diào)用的線程)都“跑”到最近的安全點上再停頓下來。這里有兩種方案可供選擇:

  • 搶先式中斷(Preemptive Suspension):不需要線程的執(zhí)行代碼主動去配合,在GC發(fā)生時,首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。(現(xiàn)在幾乎沒有虛擬機實現(xiàn)采用搶先式終端來暫停線程從而響應GC事件)
  • 主動式中斷:當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執(zhí)行時主動去輪詢這個標志,發(fā)現(xiàn)中斷標志為真時就自己中斷掛起.輪詢標志的地方和安全點是重合的,另外再加上創(chuàng)建對象需要分配內(nèi)存的地方。
0x01b6d627:call 0x01b2b210;OopMap{[60]=Oop off=460} ;*invokeinterface size ;-Client1:main@113(line 23)
;{virtual_call}
0x01b6d62c:nop ;OopMap{[60]=Oop off=461} ;*if_icmplt ;-Client1:main@118(line 23)
0x01b6d62d:test%eax,0x160100;{poll}
0x01b6d633:mov 0x50(%esp),%esi
0x01b6d637:cmp%eax,%esi

上述代碼中test指令是HotSpot生成的輪詢指令,當需要暫停線程時,虛擬機把0x160100的內(nèi)存頁設置為不可讀,線程執(zhí)行到test指令時就會產(chǎn)生一個自陷異常信號,在預先注冊的異常處理器中暫停線程實現(xiàn)等待,這樣一條匯編指令便完成安全點輪詢和觸發(fā)線程中斷。

安全區(qū)域

使用安全點可能存在的問題: 是線程處于Sleep狀態(tài)或者Blocked狀態(tài),這時候線程無法響應JVM的中斷請求,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間。

安全區(qū)域:在一段代碼片段之中,引用關(guān)系不會發(fā)生變化.在這個區(qū)域中的任何地方開始GC都是安全的.也可以把Safe Region看作是被擴展了的SafePoint

在程序執(zhí)行到Safe Region中的代碼時,首先標示自己已經(jīng)進入了Safe Region,那樣當這段時間里JVM要發(fā)起GC就不用管標識自己為Safe Region狀態(tài)的線程了。在線程要離開Safe Region時,它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點枚舉(或者是整個GC過程),如果完成了,那線程就繼續(xù)執(zhí)行,否則它就必須等待直到收到可以安全離開Safe Region的信號為止。

動態(tài)對象年齡判定

虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡

空間分配擔保

在發(fā)生Minor GC之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行
一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。

下面解釋一下“冒險”是冒了什么風險,前面提到過,新生代使用復制收集算法,但為了內(nèi)存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現(xiàn)大量對象在MinorGC后仍然存活的情況(最極端的情況就是內(nèi)存回收后新生代中所有對象都存活),就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下來在實際完成內(nèi)存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經(jīng)驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

取平均值進行比較其實仍然是一種動態(tài)概率的手段,也就是說,如果某次Minor GC存活后的對象突增,遠遠高于平均值的話,依然會導致?lián)J。℉andle Promotion Failure)。如果出現(xiàn)了HandlePromotionFailure失敗,那就只好在失敗后重新發(fā)起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關(guān)打開,避免Full GC過于頻繁

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容