Tip-關於JVM和Hotspot,你也許有這麼幾個容易暈的問題

徐家三少發表於2017-04-23

1.JVM的結構到底有哪些?

       快速過一遍JVM的記憶體結構,JVM中的記憶體分為5個虛擬的區域:

Tip-關於JVM和Hotspot,你也許有這麼幾個容易暈的問題



▪ 你的Java程式中所分配的每一個物件都需要儲存在記憶體裡。堆是這些例項化的物件所儲存的地方。是的——都怪new操作符,是它把你的Java堆都佔滿了的!
▪ 它由所有執行緒共享
▪ 當堆耗盡的時候,JVM會丟擲java.lang.OutOfMemoryError 異常
▪ 堆的大小可以通過JVM選項-Xms和-Xmx來進行調整

堆被分為:

▪ Eden區 —— 新物件或者生命週期很短的物件會儲存在這個區域中,這個區的大小可以通過-XX:NewSize和-XX:MaxNewSize引數來調整。新生代GC(垃圾回收器)會清理這一區域。
▪ Survivor區 —— 那些歷經了Eden區的垃圾回收仍能存活下來的依舊存在引用的物件會待在這個區域。這個區的大小可以由JVM引數-XX:SurvivorRatio來進行調節。
▪ 老年代 —— 那些在歷經了Eden區和Survivor區的多次GC後仍然存活下來的物件(當然了,是拜那些揮之不去的引用所賜)會儲存在這個區裡。這個區會由一個特殊的垃圾回收器來負責。年老代中的物件的回收是由老年代的GC(major GC)來進行的。
方法區
▪ 也被稱為非堆區域(在HotSpot JVM的實現當中)
▪ 它被分為兩個主要的子區域
持久代
       這個區域會 儲存包括類定義,結構,欄位,方法(資料及程式碼)以及常量在內的類相關資料。它可以通過-XX:PermSize及 -XX:MaxPermSize來進行調節。如果它的空間用完了,會導致java.lang.OutOfMemoryError: PermGen space的異常。
程式碼快取
       這個快取區域是用來儲存編譯後的程式碼。編譯後的程式碼就是原生程式碼(硬體相關的),它是由JIT(Just In Time)編譯器生成的,這個編譯器是Oracle HotSpot JVM所特有的。
JVM棧
▪ 和Java類中的方法密切相關
▪ 它會儲存區域性變數以及方法呼叫的中間結果及返回值
▪ Java中的每個執行緒都有自己專屬的棧,這個棧是別的執行緒無法訪問的。
▪ 可以通過JVM選項-Xss來進行調整
本地棧
▪ 用於本地方法(非Java程式碼)
▪ 按執行緒分配
PC暫存器
▪ 特定執行緒的程式計數器
▪ 包含JVM正在執行的指令的地址(如果是本地方法的話它的值則未定義)

2.為什麼JVM規範裡面從來沒有出現過永久代這個詞?

        根據 JVM 規範,JVM 記憶體共分為虛擬機器棧、堆、方法區、程式計數器、本地方法棧五個部分:

Tip-關於JVM和Hotspot,你也許有這麼幾個容易暈的問題

        絕大部分 Java 程式設計師應該都見過 "java.lang.OutOfMemoryError: PermGen space "這個異常。這裡的 “PermGen space”其實指的就是方法區。前者是 JVM 的規範,而後者則是 JVM 規範的一種實現,並且只有 HotSpot 才有 “PermGen space”,而對於其他型別的虛擬機器,如 JRockit(Oracle)、J9(IBM) 並沒有“PermGen space”。由於方法區主要儲存類的相關資訊,所以對於動態生成類的情況比較容易出現永久代的記憶體溢位。最典型的場景就是,在 jsp 頁面比較多或者載入的包較多的情況,容易出現永久代記憶體溢位。因此,你會看到Java8虛擬機器規範和Java7並沒有什麼不同,但是所有人都在告訴你:Hotspot虛擬機器捨棄了永久代。

3.JDK7中的永久代會回收嗎?它回收哪些東西?

       一般的垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(FullGC)。如果仔細檢視垃圾收集器的輸出資訊,就會發現永久代也是被回收的。那麼永久代能回收什麼呢?一,廢棄的字串常量,二,不再被引用的class物件。正確的設定永久代大小對避免FullGC是非常重要的原因。所以引起full GC的原因不光是老年代滿了或者超過臨界值,也有可能是永久代滿了或者超過臨界值。

4.永久代已經沒了。那麼JVM規範中的方法區HotSpot是如何實現的?

       JDK8的Hotspot使用Metaspace(元空間)代替了永久代。相比較永久代,有如下的一些改變。這個改變在如下文章中有描述:
blogs.oracle.com/poonam/entr…

       其中有如下一段話:

元空間被直接接分配在本地記憶體。預設的後設資料空間分配只受到本地記憶體的限制。我們可以使用一個新的MaxMetaspaceSize選項來設定元空間佔用本地記憶體的大小。這個選項與MaxPermSize類似。當元空間使用量MetaspaceSize設定的值(32位的client模式預設是12M,32位的server模式是16M,64位的server模式則會更多)達到了垃圾收集器會收集那些不再使用的classloader和class會被回收。所有設定一個較大的MetaspaceSize值會延遲垃圾收集發生的時間。在觸發一次垃圾收集以後和下一次垃圾收集之前,元空間的使用值值會隨著使用不斷增加。

       如果要想知道關於元空間更多的內容,可以訪問這個連結,這個連結專門講述了關於元空間一些更多的細節。

5.Hotpsot垃圾回收的原理是什麼?

       hotspot的垃圾收集的判定主要是使用可達性分析演算法。在目前主流的程式語言(java,C#等)的主流實現中,都是稱通過可達性分析(Reachability Analysis)來判定物件是否存活的。這個演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個物件不可達)時,則證明此物件是不可用的。如下圖所示,物件object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的物件。

Tip-關於JVM和Hotspot,你也許有這麼幾個容易暈的問題

僅僅是瞭解這些是不夠的。在我的部落格中,有一篇JAVA記憶體白皮書的翻譯java記憶體管理白皮書,非常經典。

6.被判定為垃圾地物件一定會被回收嗎?

       即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:
       如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”。(即意味著直接回收)

       如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會放置在一個叫做F-Queue的佇列之中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。這裡所謂的“執行”是指虛擬機器會觸發這個方法,但並不承諾會等待它執行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死迴圈(更極端的情況),將很可能會導致F-Queue佇列中其他物件永久處於等待,甚至導致整個記憶體回收系統崩潰。
finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個物件建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果物件這時候還沒有逃脫,那基本上它就真的被回收了。
程式碼示例:

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方法優先順序很低,所以暫停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方法優先順序很低,所以暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,i am dead:(");
        }
    }
}複製程式碼

執行結果:

finalize mehtod executed!
yes,i am still alive:)
no,i am dead:(複製程式碼

       SAVE_HOOK物件的finalize()方法確實被GC收集器觸發過,並且在被收集前成功逃脫了。另外一個值得注意的地方是,程式碼中有兩段完全一樣的程式碼片段,執行結果卻是一次逃脫成功,一次失敗,這是因為任何一個物件的finalize()方法都只會被系統自動呼叫一次,如果物件面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段程式碼的自救行動失敗了。因為finalize()方法已經被虛擬機器呼叫過,虛擬機器都視為“沒有必要執行”(即意味著直接回收)。

相關文章