深入理解java虛擬機—— 虛擬機 深扒垃圾回收器,再也不怕BAT面試

目錄

下面開始本篇主要介紹的內容:

Java虛擬機垃圾回收

 ------ 爲什麼需要了解垃圾回收

 ------ GC回收那些對象,何時回收,如何回收

           ------ 1、垃圾回收器回收那些內存?

           ------ 2、什麼時候回收?即GC發生在什麼時候?

           ------ 3、如何回收?

關於GC什麼時候回收?即GC發生在什麼時候?

 ------ 判斷可回收有2種方式:

           ------ 1、引用計數算法(Recference Counting)

           ------ 2、可達性分析算法(Reachability Analysis)

 ------ 再談引用

 ------ 對象生存還是死亡?

           ------ 判斷對象死亡的條件

           ------ 一次對象的自我救贖

 ------ 再次解析 JVM虛擬機 可達性的實現

           ------ 1、可達性分析的問題

           ------ 2、枚舉根節點查找GC Roots

                 安全點

                 安全區域

參考文獻


 

1、前篇介紹了【 JAVA虛擬機堆內存結構以及堆內存作用對象回收機制 】,主要包含四部分

    一、堆區(Heap) 

    二、對象的內存佈局

    三、對象的訪問定位

    四、Java堆的內存劃分

2、前篇博文已將對JVM虛擬機內存中的 方法棧 【JAVA虛擬機內存結構之虛擬機棧(JVM Stack)】做了詳細的介紹,棧的四大部分:

虛擬機棧主要用於存儲四部分內容

棧幀(Stack Frame)

        ------ 局部變量表

        ------ 操作數棧

        ------ 動態連接

        ------ 方法返回地址

想了解棧的內存結構,已將棧的運行原理,可以去看一下。

3、JAVA虛擬機程序計數器深度解析 【JAVA虛擬機程序計數器深度解析

        ------ 程序計數器(Program Counter Register)

        ------ JAVA虛擬機多線程的執行過程

        ------ java多線程下程序計數器如何起作用的

 

想了解JVM整體內存架構的可以看一下這篇博文  【JAVA虛擬機的整體內存模型】,可以從整體瞭解虛擬機的組成,以及各部分功能如何組合在一起工作的。

 

下面開始本篇主要介紹的內容

 

Java虛擬機垃圾回收

 

在堆裏面存放着Java世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”着,哪些已經“死去”(“死去”即不可能再被任何途徑使用的對象)了。

jvm 中,程序計數器、虛擬機棧、本地方法棧都是隨線程而生隨線程而滅,棧幀隨着方法的進入和退出做入棧和出棧操作,實現了自動的內存清理,因此,我們的內存垃圾回收 主要集中於 java 堆和方法區中 ,在程序運行期間,這部分內存的分配和使用都是動態的.

 

爲什麼需要了解垃圾回收


       目前內存的動態分配與內存回收技術已經相當成熟,但爲什麼還需要去了解內存分配與GC呢?

       1、當需要排查各種內存溢出、內存泄漏問題時;

       2、當垃圾收集成爲系統達到更高併發量的瓶頸時;

       我們就需要對這些"自動化"技術實話必要的監控和調節;
 

GC回收那些對象,何時回收,如何回收

 

       1、垃圾回收器回收那些內存?

            即如何判斷對象已經死亡可以回收? ==》 需要了解回收策略;

       2、什麼時候回收?即GC發生在什麼時候?

             關於GC什麼時候回收 ==》 需要了解GC策略,與垃圾回收器實現有關;

       3、如何回收?

             回收對象的算法,回收的方式 ==》 即需要了解垃圾回收算法,及算法的實現--垃圾回收器;

 

 

內存回收系統模塊以及各部分的功能,以及垃圾回收器在堆內存哪部分使用:

 

 

 

關於GC什麼時候回收?即GC發生在什麼時候?

 

垃圾收集器對堆進行回收前,首先要確定堆中的對象哪些還"存活",哪些已經"死去";

下面先來了解兩種判斷對象不再被引用的算法,再來談談對象的引用,最後來看如何真正宣告一個對象死亡。

 

判斷可回收有2種方式:

 

1、引用計數算法(Recference Counting)

很多教科書判斷對象是否存活的算法是這樣的:在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器爲零的對象就是不可能再被使用的。筆者面試過很多應屆生和一些有多年工作經驗的開發人員,他們對於這個問題給予的都是這個答案。

主流的JVM都沒有選用引用計數算法來管理內存;

客觀地說,引用計數算法(Reference Counting)雖然佔用了一些額外的內存空間來進行計數,但它的原理簡單,判定效率也很高,在大多數情況下它都是一個不錯的算法。也有一些比較著名的應用案例,例如微軟COM(Component Object Model)技術、使用ActionScript 3的FlashPlayer、Python語言以及在遊戲腳本領域得到許多應用的Squirrel中都使用了引用計數算法進行內存管理。但是,在Java領域,至少主流的Java虛擬機裏面都沒有選用引用計數算法來管理內存,主要原因是,這個看似簡單的算法有很多例外情況要考慮,必須要配合大量額外處理才能保證正確地工作,譬如單純的引用計數就很難解決對象之間相互循環引用的問題。

 

優點

      實現簡單,判定高效,可以很好解決大部分場景的問題,也有一些著名的應用案例;

缺點

(A)、很難解決對象之間相互循環引用的問題

(B)、並且開銷較大,頻繁且大量的引用變化,帶來大量的額外運算;

ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;

       當兩個對象不再被訪問時,因爲相互引用對方,導致引用計數不爲0;

       更復雜的循環數據結構,如圖(《編譯原理》7-18):

 

證明Java虛擬機裏面都沒有選用引用計數算法來管理內存:

 

因此:

主流的JVM都沒有選用引用計數算法來管理內存;在Java領域,至少主流的Java虛擬機裏面都沒有選用引用計數算法來管理內存。

微軟COM(Component Object Model)技術、使用ActionScript 3的FlashPlayer、Python語言以及在遊戲腳本領域得到許多應用的Squirrel中都使用了引用計數算法進行內存管理

 

2、可達性分析算法(Reachability Analysis)

當前主流的商用程序語言Java、C#,上溯至前面提到的古老的Lisp)的內存管理子系統,都是通過可達性分析(Reachability Analysis)算法來判定對象是否存活的。這個算法的基本思路就是通過一系列稱爲“GC Roots”的根對象作爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲“引用鏈”(ReferenceChain),如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。

 

算法基本思路:

      通過一系列"GC Roots"對象作爲起始點,開始向下搜索;

      搜索所走過和路徑稱爲引用鏈(Reference Chain);

      當一個對象到GC Roots沒有任何引用鏈相連時(從GC Roots到這個對象不可達),則證明該對象是不可用的;

 

哪些對象可以做GC Roots對象

      Java中,GC Roots對象包括:

  1. 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
  2. ·在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
  3. ·在方法區中常量引用的對象,譬如字符串常量池(String Table)裏的引用。
  4. ·在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
  5. ·Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
  6. ·所有被同步鎖(synchronized關鍵字)持有的對象。
  7. ·反映Java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

除了這些固定的GC Roots集合以外:

根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,還可以有其他對象“臨時性”地加入,共同構成完整GC Roots集合。譬如後文將會提到的分代收集和局部回收(Partial GC),如果只針對Java堆中某一塊區域發起垃圾收集時(如最典型的只針對新生代的垃圾收集),必須考慮到內存區域是虛擬機自己的實現細節(在用戶視角里任何內存區域都是不可見的),更不是孤立封閉的,所以某個區域裏的對象完全有可能被位於堆中其他區域的對象所引用,這時候就需要將這些關聯區域的對象也一併加入GC Roots集合中去,才能保證可達性分析的正確性。

目前最新的幾款垃圾收集器無一例外都具備了局部回收的特徵,爲了避免GC Roots包含過多對象而過度膨脹,它們在實現上也做出了各種優化處理。關於這些概念、優化技巧以及各種不同收集器實現等內容

 

優點

  •       更加精確和嚴謹,可以分析出循環數據結構相互引用的情況;

缺點

  •       實現比較複雜;
  •       需要分析大量數據,消耗大量時間;
  •      分析過程需要GC停頓(引用關係不能發生變化),即停頓所有Java執行線程(稱爲"Stop The World",是垃圾回收重點關注的問題);

後面會針對HotSpot虛擬機實現的可達性分析算法進行介紹,看看是它如何解決這些缺點的。
 

再談引用

 

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否引用鏈可達,判定對象是否存活都和“引用”離不開關係。

在JDK 1.2版之後,Java對引用的概念進行了擴充,將引用分爲:

 

  • 強引用(Strongly Re-ference)

·強引用是最傳統的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關係。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。

  • 軟引用(Soft Reference)

·軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2版之後提供了SoftReference類來實現軟引用。

對於軟引用,可以使用命令行選項"-XX:SoftRefLRUPolicyMSPerMB = <N>"來控制清除速率;

  • 弱引用(Weak Reference)

·弱引用也是用來描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生爲止。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2版之後提供了WeakReference類來實現弱引用。

  • 虛引用(Phantom Reference)

·虛引用也稱爲“幽靈引用”或者“幻影引用”,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的只是爲了能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2版之後提供了PhantomReference類來實現虛引用。

4種,這4種引用強度依次逐漸減弱。

 

對象生存還是死亡?

即使在可達性分析算法中判定爲不可達的對象,也不是“非死不可”的,這時候它們暫時還處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨後進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,那麼虛擬機將這兩種情況都視爲“沒有必要執行”。

如果這個對象被判定爲確有必要執行finalize()方法,那麼該對象將會被放置在一個名爲F-Queue的隊列之中,並在稍後由一條由虛擬機自動建立的、低調度優先級的Finalizer線程去執行它們的finalize()方法。這裏所說的“執行”是指虛擬機會觸發這個方法開始運行,但並不承諾一定會等待它運行結束。這樣做的原因是,如果某個對象的finalize()方法執行緩慢,或者更極端地發生了死循環,將很可能導致F-Queue隊列中的其他對象永久處於等待,甚至導致整個內存回收子系統的崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後收集器將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。

 

finalize()方法

上面已經說到finalize()方法與垃圾回收第二次標記相關,下面瞭解下在Java語言層面有哪些需要注意的。

 finalize()是Object類的一個方法,是Java剛誕生時爲了使C/C++程序員容易接受它所做出的一個妥協,但不要當作類似C/C++的析構函數;

 因爲它執行的時間不確定,甚至是否被執行也不確定(Java程序的不正常退出),而且運行代價高昂,無法保證各個對象的調用順序(甚至有不同線程中調用);

 如果需要"釋放資源",可以定義顯式的終止方法,並在"try-catch-finally"的finally{}塊中保證及時調用,如File相關類的close()方法;
 

finalize()方法主要有兩種用途:

1、充當"安全網"

      當顯式的終止方法沒有調用時,在finalize()方法中發現後發出警告;

      但要考慮是否值得付出這樣的代價;

      如FileInputStream、FileOutputStream、Timer和Connection類中都有這種應用;

2、與對象的本地對等體有關

      本地對等體:普通對象調用本地方法(JNI)委託的本地對象;

      本地對等體不會被GC回收;

      如果本地對等體不擁有關鍵資源,finalize()方法裏可以回收它(如C/C++中malloc(),需要調用free());

      如果有關鍵資源,必須顯式的終止方法;

      一般情況下,應儘量避免使用它,甚至可以忘掉它。

 

判斷對象死亡的條件

 要真正宣告一個對象死亡,至少要經歷兩次標記過程。

1、第一次標記

      在可達性分析後發現到GC Roots沒有任何引用鏈相連時,被第一次標記;

      並且進行一次篩選:此對象是否必要執行finalize()方法;

(A)、沒有必要執行

      沒有必要執行的情況:

      (1)、對象沒有覆蓋finalize()方法;

      (2)、finalize()方法已經被JVM調用過;

      這兩種情況就可以認爲對象已死,可以回收;

(B)、有必要執行

      對有必要執行finalize()方法的對象,被放入F-Queue隊列中;

      稍後在JVM自動建立、低優先級的Finalizer線程(可能多個線程)中觸發這個方法;
 

2、第二次標記

      GC將對F-Queue隊列中的對象進行第二次小規模標記;

  finalize()方法是對象逃脫死亡的最後一次機會:


      (A)、如果對象在其finalize()方法中重新與引用鏈上任何一個對象建立關聯,第二次標記時會將其移出"即將回收"的集合;

      (B)、如果對象沒有,也可以認爲對象已死,可以回收了;                    

      一個對象的finalize()方法只會被系統自動調用一次,經過finalize()方法逃脫死亡的對象,第二次不會再調用;
 

 

一次對象的自我救贖

上面關於對象死亡時finalize()方法的描述可能帶點悲情的藝術加工,筆者並不鼓勵大家使用這個方法來拯救對象。相反,筆者建議大家儘量避免使用它,因爲它並不能等同於C和C++語言中的析構函數,而是Java剛誕生時爲了使傳統C、C++程序員更容易接受Java所做出的一項妥協。它的運行代價高昂,不確定性大,無法保證各個對象的調用順序,如今已被官方明確聲明爲不推薦使用的語法。

 

再次解析 JVM虛擬機 可達性的實現

前面對可達性分析算法進行介紹,並看到了它在判斷對象存活與死亡的作用,下面看看是HotSpot虛擬機是如何實現可達性分析算法,如何解決相關缺點的。

1、可達性分析的問題

上文中已經簡單介紹了可達性分析的一些優缺點,缺點主要有三條:

  •       實現比較複雜;
  •       需要分析大量數據,消耗大量時間;
  •      分析過程需要GC停頓(引用關係不能發生變化),即停頓所有Java執行線程(稱爲"Stop The World",是垃圾回收重點關注的問題);

1-1、消耗大量時間

     從前面可達性分析知道,GC Roots主要在全局性的引用(常量或靜態屬性)和執行上下文中(棧幀中的本地變量表);

     去除上述的7中方式GC Roots,甚至還會根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,還可以有其他對象“臨時           性”地加入,共同構成完整GC Roots集合。

     要在這些大量的數據中,逐個檢查引用,會消耗很多時間;

1-2、GC停頓

      可達性分析期間需要保證整個執行系統的一致性,對象的引用關係不能發生變化;

      導致GC進行時必須停頓所有Java執行線程(稱爲"Stop The World");

      (幾乎不會發生停頓的CMS收集器中,枚舉根節點 時也是必須要停頓的)

 

  Stop The World:         

  在新生代進行的GC叫做minor GC,在老年代進行的GC都叫 major GC,Full GC同時作用於新生代和老年代。在垃圾回收過程中經常涉及到對對象的挪動(比如對象在Survivor 0和Survivor 1之間的複製),進而導致需要對對象引用進行更新。爲了保證引用更新的正確性,Java將暫停所有其他的線程,這種情況被稱爲“Stop-The-World”,導致系統全局停頓。Stop-The-World對系統性能存在影響,因此垃圾回收的一個原則是儘量減少“Stop-The-World”的時間。

不同垃圾收集器的Stop-The-World情況,Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情況;而即便是最新的G1收集器也不例外。

  • Java中一種全局暫停的現象,jvm掛起狀態
  • 全局停頓,所有Java代碼停止,native代碼可以執行,但不能和JVM交互

 

Stop The World的發生情況:

  • 多半由於jvm的GC引起,如:
    1.老年代空間不足。
    2.永生代(jkd7)或者元數據空間(jkd8)不足。
    3.System.gc()方法調用。
    4.CMS GC時出現promotion failed和concurrent mode failure
    5.YoungGC時晉升老年代的內存平均值大於老年代剩餘空間
    6.有連續的大對象需要分配

  • 除了GC還有以下原因:
    1.Dump線程--人爲因素。
    2.死鎖檢查。
    3.堆Dump--人爲因素。
    Full GC 是清理整個堆空間—包括年輕代和老年代。

 

爲何GC全局停頓無法避免:

當gc線程在處理垃圾的時候,其它java線程要停止才能徹底清除乾淨,否則會影響gc線程的處理效率增加gc線程負擔,特別是在垃圾標記的時候。

 

GC全局停頓帶來的危害:

  • 長時間服務停止,沒有響應
  • 遇到HA系統,可能引起主備切換,嚴重危害生產環境。
  • 新生代的gc時間比較短(),危害小。
  • 老年代的gc有時候時間短,但是有時候比較長几秒甚至100秒--幾十分鐘都有。
  • 堆越大花的時間越長。

  2、枚舉根節點查找GC Roots

可作爲GC Roots根節點的對象主要是在全局性的引用(如常量、類靜態屬性)和執行上下文中(如棧幀中的本地變量表),現在的很多應用僅方法區就有數百兆,逐個檢查裏邊的引用顯然很耗費時間。

目前主流JVM都是準確式GC,可以直接得知哪些地方存放着對象引用,所以執行系統停頓下來後,並不需要全部、逐個檢查完全局性的和執行上下文中的引用位置;

在HotSpot實現中,利用了空間換取時間,是使用一組OopMap的數據結構來完成的。類加載完成後,會把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,在特定的位置(即安全點)使用OopMap記錄下棧和寄存器哪些位置是引用,這樣GC發生的時候就不用全部掃描了。

實際上HotSpot也的確沒有爲每條指令都生成OopMap,前面已經提到,只是在“特定的位置”記錄了這些信息,這些位置被稱爲安全點(Safepoint)

梳理枚舉GC Roots實現:

在HotSpot中,是使用一組稱爲OopMap的數據結構來達到這個目的的;

      在類加載時,計算對象內什麼偏移量上是什麼類型的數據;

      在JIT編譯時,也會記錄棧和寄存器中的哪些位置是引用;

      這樣GC掃描時就可以直接得知這些信息;

 

 

注意:

實際上HotSpot也的確沒有爲每條指令都生成OopMap,前面已經提到,只是在“特定的位置”記錄了這些信息,這些位置被稱爲安全點(Safepoint)。有了安全點的設定,也就決定了用戶程序執行時並非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強制要求必須執行到達安全點後才能夠暫停。因此,安全點的選定既不能太少以至於讓收集器等待時間過長,也不能太過頻繁以至於過分增大運行時的內存負荷。安全點位置的選取基本上是以“是否具有讓程序長時間執行的特徵”爲標準進行選定的,因爲每條指令執行的時間都非常短暫,程序不太可能因爲指令流長度太長這樣的原因而長時間執行,“長時間執行”的最明顯特徵就是指令序列的複用,例如方法調用、循環跳轉、異常跳轉等都屬於指令序列複用,所以只有具有這些功能的指令纔會產生安全點。

 

安全點

  如果每條指令都生成OopMap那將需要大量的額外空間。此時在特定的位置即安全點使程序不是在所有的地方都停頓(Stop The World)下來開始GC,只有在到達安全點時才停頓開始GC。安全點的選定不能使GC等待時間過長也不能使GC頻繁觸發,如具有方法調用、循環跳轉、異常跳轉的指令纔會有安全點。

 

首先看爲何讓所有的線程都跑到安全點附近停頓下來??

參考: 理解進入safepoint時如何讓Java線程全部阻塞

VM thread在進行GC前,必須要讓所有的Java線程阻塞,從而stop the world,開始標記。JVM採用了主動式阻塞的方式,Java線程不是隨時都可以進入阻塞,需要運行到特定的點,叫safepoint,在這些點的位置Java線程可以被全部阻塞,整個堆的狀態是一個暫時穩定的狀態,OopMap指出了這個時刻,寄存器和棧內存的哪些具體的地址是引用,從而可以快速找到GC roots來進行對象的標記操作。

那麼當Java線程運行到safepoint的時候,JVM如何讓Java線程掛起呢?這是一個複雜的操作。很多文章裏面說了JIT編譯模式下,編譯器會把很多safepoint檢查的操作插入到編譯偶的指令中,比如下面的指令來自內存篇:JVM內存回收理論與實現 

 

 另一個問題是GC發生時如何讓所有的線程都跑到安全點附近停頓下來?兩種方案:搶先式中斷和主動式中斷。 

       搶先式中斷,GC發生時首先讓所有的線程都停頓下來,然後讓還沒到安全點的線程恢復,跑到安全點。幾乎沒有虛擬機採用這種方式響應GC。

  主動式中斷,GC發生需要中斷所有的線程時,不直接對線程操作而是設置一箇中斷標誌,該標誌和安全點位置重合,各線程執行時都會去輪詢該中斷標誌,如果線程發現該標誌爲真時就自己中斷掛起。

 

1、安全點是什麼,爲什麼需要安全點

      HotSpot在OopMap的幫助下可以快速且準確的完成GC Roots枚舉,但是這有一個問題:        

      運行中,非常多的指令都會導致引用關係變化;

      如果爲這些指令都生成對應的OopMap,需要的空間成本太高;

      問題解決:

      只在特定的位置記錄OopMap引用關係,這些位置稱爲安全點(Safepoint);

      即程序執行時並非所有地方都能停頓下來開始GC;

2、安全點的選定

      不能太少,否則GC等待時間太長;也不能太多,否則GC過於頻繁,增大運行時負荷;

      所以,基本上是以程序"是否具有讓程序長時間執行的特徵"爲標準選定;

      "長時間執行"最明顯的特徵就是指令序列複用,如:方法調用、循環跳轉、循環的末尾、異常跳轉等;

      只有具有這些功能的指令纔會產生Safepoint;

 

3、如何在安全點上停頓

      對於Safepoint,如何在GC發生時讓所有線程(不包括JNI線程)運行到其所在最近的Safepoint上再停頓下來?

      主要有兩種方案可選:

(A)、搶先式中斷(Preemptive Suspension)

      不需要線程主動配合,實現如下:

      (1)、在GC發生時,首先中斷所有線程;

      (2)、如果發現不在Safepoint上的線程,就恢復讓其運行到Safepoint上;

      現在幾乎沒有JVM實現採用這種方式;

(B)、主動式中斷(Voluntary Suspension)

      (1)、在GC發生時,不直接操作線程中斷,而是僅簡單設置一個標誌;

      (2)、讓各線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起;

      而輪詢標誌的地方和Safepoint是重合的;    

      在JIT執行方式下:test指令是HotSpot生成的輪詢指令;

      一條test彙編指令便完成Safepoint輪詢和觸發線程中斷;
 

 

安全區域

  程序運行時安全點可以完美的解決GC觸發的問題,但是當程序不執行時即沒有分配到CPU時間時,此時線程不能響應JVM的中斷請求,JVM顯然不可能等待線程分配到CPU時間跑到安全點時再開始GC。這是需要安全區域來解決問題。

  安全區域是指一段代碼段中引用關係不會發生變化,在該區域何時何地開始GC都是安全的。線程執行安全區域的代碼塊時會標識自己已經進入了安全區域,此時如果JVM發起GC線程不會再標誌中斷狀態標識,線程離開安全區域時會檢查GC枚舉GC Roots根節點(或者是整個GC過程)是否已經完成,如果完成了就繼續執行,沒完成的話就等待GC完成回收任務收到可以離開的信號再離開安全區域。

1、爲什麼需要安全區域

      對於上面的Safepoint還有一個問題:

      程序不執行時沒有CPU時間(Sleep或Blocked狀態),無法運行到Safepoint上再中斷掛起;    

      這就需要安全區域來解決;

2、什麼是安全區域(Safe Region)

      指一段代碼片段中,引用關係不會發生變化;

      在這個區域中的任意地方開始GC都是安全的;

3、如何用安全區域解決問題

      安全區域解決問題的思路:

      (1)、線程執行進入Safe Region,首先標識自己已經進入Safe Region;

      (2)、線程被喚醒離開Safe Region時,其需要檢查系統是否已經完成根節點枚舉(或整個GC);

      如果已經完成,就繼續執行;

      否則必須等待,直到收到可以安全離開Safe Region的信號通知;    

這樣就不會影響標記結果;

      雖然HotSpot虛擬機中採用了這些方法來解決對象可達性分析的問題,但只是大大減少了這些問題影響,並不能完全解決,如GC停頓"Stop The World"是垃圾回收重點關注的問題,後面介紹垃圾回收器時應注意:低GC停頓是其一個關注。
 

 

 

 

下文繼續分析垃圾收集器及收集算法。

 

 

 

 

參考文獻

  • 《深入理解Java虛擬機:JVM高級特性與最佳實踐》
  • 《Java虛擬機規範 Java SE 8版》
  • 《Java併發編程的藝術》
  • 《How to Handle Java Finalization's Memory-Retention Issues》
  • 《Effective Java》第二版 第2章 第7條:避免使用終結方法;
  • 《Thinking in Java》第四版 5.5 清理:終結處理和垃圾回收;
  • 《Java語言規範》12.6 類實例的終結;
     

 

 

 

 

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