天天看點

揭秘JAVA JVM内幕(不合适初學者)

       在之前的文章 一步步解析java執行内幕 中,比較詳細分析了java代碼是如何一步一步在jvm中執行的,然而涉及到的jvm核心技術點,并未做深入分析,因為覺得那時候分享,還不是時候,慶幸的是,最近剛優化線上商城并發系統,相關優化記錄在上篇博文 記一次線上商城系統高并發的優化 ,分享這篇文章後,覺得是時候與大家分享交流jvm底層一些核心技術的時機了。

       本篇文章将重點分析jvm,涉及到的内容包括jvm記憶體模型,類加載器,GC回收算法,GC回收器,整體偏向于理論。

       本篇文章不适合初學者,适合具有3年以上開發經驗的技術人員,歡迎大家一起交流分享,文章若有不足之處,歡迎讀者朋友們指出,先感謝。

一  明确jdk,jre和jvm之間關系

   下圖為官網關于jdk,jre和jvm的架構圖,從該架構圖,很容易看出三者之間關系:

     (1)jdk包含jre,而jre又包含jvm

     (2)jdk主要用于開發環境,jre主要用于釋出環境,當然,釋出環境用jdk也沒問題,僅僅是性能可能會有點影響,jdk與jre關系有點類似程式debug版本和release版本之間關系

     (3)從檔案大小來說,jdk比jre大。從圖中可以看出,jdk比jre多了一層工具包,如常用的javac,java指令等

二   類加載器

關于jvm類加載器,可概括為如下圖:

1.為什麼要有類加載器?

(1)将位元組碼檔案加載到運作時資料區。.java源碼通過Javac指令編譯後形成的位元組碼檔案(.class),通過類加載器加載進入jvm中的。

(2)确定位元組碼檔案在運作時資料區的唯一性。相同的位元組碼檔案,通過不同的類加載器,就形成不同的檔案,是以位元組碼檔案在運作時資料區的唯一性是由位元組碼檔案和加載它的類加載器共同決定的

2.類加載器的種類

從種類上來劃分,類加載器主要劃分為四大類

(1)啟動類加載器 (根類加載器Bootstrap ClassLoader):該類加載器位于類加載器的最頂層,主要加載jre核心相關jar包,如 /jre/lib/rt.jar

(2)擴充類加載器(Extension ClassLoader):該類加載器位于類加載器層次的第二層,主要加載 jre擴充相關jar包,如/jre/lib/ext/*.jar

(3)應用程式類加載器(Application ClassLoader) App:該類加載器位于類加載器的第三層,主要加載類路徑(classpaht)下的相關jar包

(4)使用者自定義類加載器(User ClassLoader):該類加載器為使用者自定義類加載器,主要加載使用者指定的路徑下的相關jar包

3.類加載器的機制(雙親委派)

對于位元組碼的加載,類加載機制為雙親委派,什麼叫雙親委派呢?

 類加載器擷取位元組碼檔案後,不是直接加載,而是将該位元組碼檔案傳遞給其直接父級類加載器,其直接父加載器又繼續傳遞給其直接父加載器的直接父加載器,依次類推到根父加載器,若根父加載器

能加載,則加載,否則交給其直接孩子加載器加載,直接孩子加載器能加載就加載,若不能,依次類推其直接孩子類加載器,若都不能加載,最後才由使用者自定義類加載器加載。

4.jdk 1.8 如何實作類加載器?

如下為jdk 1.8 類加載器的實作,采用遞歸方式

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
      

5.破壞雙親委派模型

在某些情況下,由于受加載範圍限制,父類加載器無法加載到需要的檔案,是以父類加載器需要委托其子類加載器去加載相應的位元組碼檔案。

如在jdk中定義的資料庫驅動接口Driver,但該接口的實作卻由不同的資料庫廠商來實作,這就産生這樣一個問題:由啟動類(Bootstrap ClassLoader)

執行的DriverManager要加載實作了Driver接口的相關實作類,進而實作統一管理,但Bootstrap ClassLoader隻能加載jre/lib下的相應檔案,不能加載

由各個廠商實作的Dirver接口相關實作類(Dirver實作類是由Application ClassLoader加載),這時就需要Bootstrap ClassLoader委托其子類加載器加載Driver

來實作,進而破壞了雙親委派模型。

 三  類的生命周期

java中的類,在jvm中的生命周期,大概分為五個階段:

1.加載階段:擷取位元組碼二進制流,并将靜态存儲結構轉化成方法區的運作時資料結構,且在方法區生成相應的類對象(java.lang.Class對象),作為該類的資料通路入口。

2.連接配接階段:該階段包括三個小階段,即驗證,準備和解析三階段

   (1)驗證:確定位元組碼檔案符合虛拟機規範要求,如中繼資料驗證,檔案格式驗證,位元組碼驗證和符号驗證等

   (2)準備:為内的靜态表裡配置設定記憶體,并且設定jvm預設值,對于非靜态變量,此階段,不需配置設定記憶體。

   (3)解析:将常量池内的符号引用轉化為直接引用

3.初始化階段:類對象使用前的一些必要初始化工作

    如下引用自一位博友的觀點,個人認為解釋得很好。

在 Java 代碼中,如果要初始化一個靜态字段,我們可以在聲明時直接指派,也可以在靜态代碼塊中對其指派。

除了 final static 修飾的常量,直接指派操作以及所有靜态代碼塊中的代碼,則會被 Java 編譯器置于同一方法中,并把它命名為 < clinit > 。初始化的目的是是為标記為

常量值的字段指派,以及執行< clinit > 方法的過程。Java 虛拟機會通過加鎖來確定類的 < clinit > 方法僅被執行一次。

哪些條件會發生類初始化呢?

(1)當虛拟機啟動時,初始化使用者指定的主類(main函數);

(2)當遇到用于建立目标類執行個體的 new 指令時,初始化 new 指令的目标類;

(3)當遇到調用靜态方法的指令時,初始化該靜态方法所在的類;

(4)子類的初始化會觸發父類的初始化;

(5)如果一個接口定義了 default 方法,那麼直接實作或者間接實作該接口的類的初始化,會觸發該接口的初始化;

(6)使用反射 API 對某個類進行反射調用時,初始化這個類;

(7)當初次調用 MethodHandle 執行個體時,初始化該 MethodHandle 指向的方法所在的類。

4.使用階段:jvm中使用對象

5.解除安裝階段:将對象從jvm中解除安裝(unload),哪些條件會使jvm發生類解除安裝呢?

  (1)加載該類的類加載器被回收

  (2)該類的所有執行個體已經被回收

  (3)該類對應的java.lang.Class對象沒有任何地方被引用

四  jvm記憶體模型

1.JVM記憶體模型是怎樣的?

如下為JVM記憶體模型架構圖,由于在之前的文章中論述過,這裡就不再一 一論述,主要講解堆區。

在jdk 1.8前,堆區主要分為新生代、老年代和永久代。jdk 1.8後,去掉了永久代,增加了MetaSpace區。這裡,主要分享jdk 1.8。

根據jdk1.8,堆區邏輯抽象為三個部分:

(1)新生代:包括Eden區,S0區(也叫from區),S21(也叫TO區)

(2)老年代

(3)Metaspace區

2.新生代和老年代的記憶體大小是怎樣的?

根據官方建議,新生代占三分之一(Eden:S0:S1=8:1:1),老年代占三分之二,是以記憶體配置設定圖如下:

揭秘JAVA JVM内幕(不合适初學者)

3.GC回收是怎樣進行的?

對象先在Eden區運作,當Eden記憶體用占用滿時,Eden會進行兩個操作:回收不用的對象和将未回收對象放入s0區,此時s0區和s1區互喚名稱,即s0->s1,s1->s0,Eden區經過一次對象回收後,釋放了空間,當Eden下次再滿時,執行相同步驟,依次循環執行,當Eden區回收後,剩下的對象超過s0容量,則将出發一次Minor GC,此時将未回收的對象放入老年區,依次循環執行,當Eden區觸發Minor GC時,剩餘的對象容量大于old區剩餘容量時,則old區将觸發一次Major GC,此時便會觸發一次Full GC。需要注意的是,一般發生Major GC,基本都都會伴随一次Full GC回收,Full GC非常損耗性能,在JVM調優時,要注意。

下圖我在生産環境截的一張GC圖,監控工具VisualVM

揭秘JAVA JVM内幕(不合适初學者)

 4.垃圾回收算法有哪些?

(1)标記-清除算法

       該算法分為2個階段,即标記階段和清楚階段,首先标記所有要回收的對象,然後回收被标記的對象。該算法效率低,且容易産生記憶體碎片。

       a.效率低:需要周遊兩次記憶體,第一次标記,第二次回收被标記對象

       b.由于是非連續記憶體片段,容易産生碎片,當對象過大時,容易發生Full GC

       下圖為标記-清除算法 回收前和回收後對比示意圖

揭秘JAVA JVM内幕(不合适初學者)

(2)标記-複制算法

      該算法解決了“标記-清除”算法效率低和大部分記憶體碎片問題,它将記憶體分為大小相等的兩塊,每次隻使用其中一塊,當其中一塊需要回收時,隻需将該快區域還存活的對象複制到另一塊,然後再把該塊記憶體一次性清理掉,循環往複。

下圖為标記-複制算法回收前和回收收簡要示意圖

揭秘JAVA JVM内幕(不合适初學者)

      然而,由于年輕代大部分對象駐留時間都非常短,98%的對象都很快被回收,存活的對象非常少,不需要按照記憶體1:1來劃分,而是按照8:1:1來劃分,

将2%存活的對象放在s0(from區)即可。

       如下為按照Eden:s0:s1 =8:1:1 劃分示意圖

揭秘JAVA JVM内幕(不合适初學者)

 (3)标記-整理算法

      該算法分為兩階段,即标記和整理,首先标記所有存活對象,将這些對象向一端移動,然後直接清理掉端邊界以外的記憶體。由于老年代的對象存活時間比較長,是以适合用該算法。

标記過程仍與“标記-清除”過程一緻,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活對象向一端移動,然後直接清理掉端邊界以外的記憶體。

     如下為"标記-整理算法"回收期和回收後示意圖

揭秘JAVA JVM内幕(不合适初學者)

(4)分代收集算法

      該算法未目前jvm算法,采用分代思想,模型如下:

揭秘JAVA JVM内幕(不合适初學者)

5.常見GC回收器有哪些?

(1)SerialGC

     SerialGC又叫串行回收器,也是最基礎的GC回收器,主要适用于單核cpu,新生代采用複制算法, 老年代采用标記-壓縮算法,在運作的過程中需要暫停應用程式,

是以會造成STW問題,在JVM标注參數為:-XX:+UseSerialGC 。

(2)ParallelGC

     ParallelGC基于SerialGC,主要解決SerialGC串行問題,改為并行問題,解決多線程問題,但同樣會産生STW問題,jvm關鍵參數:

      a.-XX:+UseParNewGC,表示新生代并行(複制算法) 老年代串行(标記-壓縮)

      b.XX:+UseParallelOldGC,老年代也是并行

(3)CMS GC

      CMSGC屬于老年代回收器,采用“标記-清除算法”,不會發生STW問題,在jvm中參數設定:

     -XX:+UseConcMarkSweepGC,表示老年代使用CMS收集器

(4)Garbage First

        Garbage First面向jvm垃圾收集器 ,它滿足短時間停頓的同時達到一個高的吞吐量,适用于多核cpu和大記憶體的服務端,也是jdk9的預設垃圾回收器。

五  總結

       本篇文章在之前文章 一步步解析java執行内幕 基礎上,深入分析了JVM記憶體模型,其中重點分析了jdk,jre和jvm關系,jvm類加載器,jvm堆記憶體劃分,GC回收器和GC回收算法等,由于篇幅有限,本篇文章未分析這些技術在JVM實際調優中是如何運用的,将在接下來的文章中與大家分享。

繼續閱讀