JAVA虛擬機JVM粗淺理解

JVM的概念

JVM(Java Virtual Machine)顧名思義就是java虛擬機,他是在不同計算機平臺中構建出來的一個虛擬計算機來實現各種功能,所以他有個很重要的特點就是平臺的無關性。只要你所在的系統無論是linux,windows亦或是macOs,只要你裝了jvm就可以運行java程序。舉個不恰當的例子,比亞迪是汽車車廠,格力是空調廠,但是他們都改裝了口罩生產線,都可以生產口罩,jvm就相當於口罩生產線,生產口罩就是運行java程序,生產場地、配置、資質相當於你計算機的配置,工廠規模的大小類似於服務器的配置能夠決定口罩的產量。

結構組成

JVM主要由三個子系統構成,分別爲類加載子系統運行時數據區執行引擎

類加載子系統

我們在創建一個java類的時候會產生一個.java的文件,通過javac編譯成.class的字節碼文件,這個過程就是將給我們看的文件轉換成給機器看的文件,只有.class文件才能被加載到。類加載子系統包含了三個步驟(加載,連接,初始化)。下圖是一個類完整的生命週期,前三個步驟是加載步驟。

加載

加載的意思就是講.calss文件加載到內存

連接

連接階段是由三部分組成,分別是驗證、準備和解析

驗證階段是驗證字節碼文件的準確性,是否符合java類文件的固定格式,語意是否符合jvm規範,字節碼是否可以被jvm安全的執行。

準備階段是類的靜態變量分配內存,然後賦予默認值,值得一提的是靜態常量則是在這一步就賦值。如下圖所示,age作爲靜態變量最初講賦予0的默認值,性別作爲靜態常量將在這一步賦值male。

解析

解析階段,jvm會把類的二級制數據中的符號引用替換爲直接引用。這是什麼意思呢?我們看到User類裏有個play的方法,ClassRoom類裏有一個User類的變量,並且有個userPlay方法引用了User裏的play方法,在字節碼文件中這只是一個描述性的符號引用。我們知道創建User類的時候會在方法區開闢一個內存存放play的方法並且對應一個內存地址,通過解析ClassRoom在引用User類裏的方法時會直接通過一個指針指向play的內存地址。符號引用就像是你想要去一個地方只是一個願望,直接引用就是將你的願望轉化成了一張機票帶你去實現願望。

類加載器

加載一個類需要類加載器,一個類只會被加載一次,加載器通過路徑來加載相應的類,如果訪問不到該類,jvm怎麼拋出classNotFound的異常。理解類加載機制對後序一些設計模式的理解,源碼分析都有幫助。JVM預定義有三種類加載器:根類加載器(bootstrap class loader)、擴展類加載器(extensions class loader)、系統類加載器(system class loader)。用戶也可以自己定義加載器。

根類加載器(bootstrap class loader)

用來加載 Java 的核心類(JAVA_HOME/jre/lib/rt.jar下的所有類),開發者無法直接獲取到啓動類加載器的引用,所以不允許直接通過引用進行操作。

擴展類加載器(extensions class loader)

用來加載JAVA_HOME/jre/lib/ext文件夾下的所有類或者由系統變量-Djava.ext.dir指定位置中的類,開發者可以直接用它來加載自己導入的jar包

系統類加載器(system class loader)

用來加載用戶自定義的類。

如上圖所示,User類,Map類,JsonObject分別使用了不同的加載器加載。

類加載機制

雙親委派:先讓父類加載器試圖加載該Class,只有父加載器無法完成此加載任務時,才自己去加載,這樣做的目的是爲了防止重複加載。以下是加載類方法的源碼:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,查看這個類之前有沒有加載到內存,如果有直接返回
            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) {                    
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

下圖是四種加載器的關係圖

全盤負責:全盤負責是指當一個ClassLoader加載一個類時,除非顯示地使用另一個ClassLoader,則該類所依賴與引用的類也由這個ClassLoader加載。

運行時數據區(內存結構)

運行時數據區分爲五部分:方法區、堆、JAVA虛擬機棧、本地方法棧和程序計數器。其中方法區和堆是共享線程,JAVA虛擬機棧、本地方法棧和程序計數器爲私有線程。共享線程是所有線程在運行時可以訪問這一部分內存,比如A線程和B線程都可以在堆內存創建一個實例對象,並且可以調用方法區的方法,這一部分數據的生命週期並不會隨着線程的終止而終止,他是和垃圾收集機制(GC)有關。私有線程的數據和線程的生命週期一致。

方法區

類的所有字段和方法字節碼,以及一些特殊方法如構造函數,接口代碼也在這裏定義。簡單來說,所有定義的方法的 信息都保存在該區域,靜態變量+常量+類信息(構造方法/接口定義)+運行時常量池都存在方法區中。

堆是用來存放對象實例的,也是佔內存最大的一塊區域。如果對象過大過多會出現內存溢出的情況,他也是GC最主要的管理區域。它分爲新生代、老年代和元空間(jdk1.8以後沒有持久代)。

GC機制

首先在伊甸區存在四個活着的對象abcd,其他區域均爲空

此時在堆內存中又創建了ef兩個對象,此時伊甸區內存佔滿,這是發生第一次minor GC,通過算法判斷a對象已經消亡,則將他回收,bcdef放入from區,並且清空伊甸區

對內存又創建了g h對象,並且又把伊甸區內存佔滿,則進行上一步驟,如果此時from內存也滿了,則將from區消亡對象清空(bcd),並將存活對象複製到to區 ,然後清空伊甸區和from區,並且將複製到to區的對象計數+1,from和to互換

當minor GC到達一定次數 from區對象(efgh)的標記次數到達15次(默認),這部分存活對象將被放入老年代

當老年代內存滿了會進行一次major GC,清空老年代,進行major GC是會造成線程停頓較長時間,所以major GC的次數儘量避免,這也是JVM調優的重要指標

未完待續

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