天天看點

java jvm學習筆記十三(jvm基本結構)

歡迎裝載請說明出處:http://blog.csdn.net/yfqnihao

                      這一節,主要來學習jvm的基本結構,也就是概述。說是概述,内容很多,而且概念量也很大,不過關于概念方面,你不用擔心,我完全有信心,讓概念在你的腦子裡變成圖形,是以隻要你有耐心,仔細,認真,并發揮你的想象力,這一章之後你會充滿自信。當然,不是說看完本章,就對jvm了解了,jvm要學習的知識實在是非常的多。在你看完本節之後,後續我們還會來學jvm的細節,但是如果你在學習完本節的前提下去學習,再學習其他jvm的細節會事半功倍。

                      為了讓你每一個知識點都有迹可循,希望你按照我的步驟一步步繼續。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

                      知識點1:什麼是java虛拟機(你以為你知道,如果你看我下面的例子,你會發現你其實不知道)

                       第一步:先來寫一個類:  

package test;

public class JVMTestForJava {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10000000);
}
}      

                       第二步:cmd視窗輸入:java test.JVMTestForJava

                       第三步:打開任務管理器-程序

 你看到一個叫java.exe的程式沒有,是滴這個就是java的虛拟機,java xxx這個指令就是用來啟動一個java虛拟機,而main函數就是一個java應用的入口,main函數被執行時,java虛拟機就啟動了。好了ctrl+c結束你的jvm。

                        第四步:打開你的ecplise,右鍵run application,再run application一次

                        第五步:打開任務管理器-程序

好了,我已經圈出來了,有兩個javaw.exe,為什麼會有兩個?因為我們剛才運作了兩次run application。這裡我是要告訴你,一個java的application對應了一個java.exe/javaw.exe(java.exe和javaw.exe你可以把它看成java的虛拟機,一個有視窗界面一個沒有)。你運作幾個application就有幾個java.exe/javaw.exe。或者更加具體的說,你運作了幾個main函數就啟動了幾個java應用,同時也啟動了幾個java的虛拟機。

                        知識點1總結:

                         什麼是java虛拟機,什麼是java的虛拟機執行個體?java的虛拟機相當于我們的一個java類,而java虛拟機執行個體,相當我們new一個java類,不過java虛拟機不是通過new這個關鍵字而是通過java.exe或者javaw.exe來啟動一個虛拟機執行個體。

                        看了上面我的描述方式,你覺得如何?概念需要背嗎?如果你對我的筆記有信心,繼續看下去吧!

                        知識點2:jvm的生命周期

                         基本上學習一種容器(更具體的說我們在學習servlet的時候),我們都要學習它的生命周期。那麼jvm的生命周期如何,我一慣不喜歡丢概念,是以來實驗,實踐出真知,老師說過的,對不!

                         第一步:copy我代碼

package test;

public class JVMTestLife {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<5;i++){
                    try {
                        Thread.currentThread().sleep(i*10000);
                        System.out.println("睡了"+i*10+"秒");
                    } catch (InterruptedException e) {
                        System.out.println("幹嘛吵醒我");
                    }
                }
            }
        }).start(); 
        
        for(int i=0;i<50;i++){
                System.out.print(i);
        }
    }
}      

                      第二步:ecplise裡run application

                      第三步:打開任務管理器-程序,看到一個javaw.exe的虛拟機在跑

                    第四步:檢視控制台輸出,并觀察任務管理器中的javaw.exe什麼時候消失

0 睡了0秒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 睡了10秒
睡了20秒
睡了30秒
睡了40秒      

這是我ecplise裡的輸出結果,而如果你觀察控制台和任務管理器的javaw.exe會發現,當main函數的for循環列印完的時候,程式居然沒有退出,而等到整個new Thread()裡的匿名類的run方法執行結束後,javaw.exe才退出。我們知道在c++的win32程式設計(CreatThread()),main函數執行完了,寄宿線程也跟着退出了,在c#中如果你用線程池(ThreadPool)的話,結論也是如此,線程都跟着宿主程序的結束而結束。但是在java中貌似和我們的認知有很大的出入,這是為什麼呢?

                    這是由于java的虛拟機種有兩種線程,一種叫叫守護線程,一種叫非守護線程,main函數就是個非守護線程,虛拟機的gc就是一個守護線程。java的虛拟機中,隻要有任何非守護線程還沒有結束,java虛拟機的執行個體都不會退出,是以即使main函數這個非守護線程退出,但是由于在main函數中啟動的匿名線程也是非守護線程,它還沒有結束,是以jvm沒辦法退出(有沒有想幹壞事的感覺??)。

                   知識點2總結:java虛拟機的生命周期,當一個java應用main函數啟動時虛拟機也同時被啟動,而隻有當在虛拟機執行個體中的所有非守護程序都結束時,java虛拟機執行個體才結束生命。

                 知識點三:java虛拟機的體系結構(無奈,我懷着悲痛心情告訴你,我們必須來一些概念,别急,咱有圖)

看到這個圖沒,名詞不是普通滴多,先來看看哪些名詞我們之前是說過的,執行引擎(筆記一),類裝載器(筆記二),java棧(筆記十一)。

在了解jvm的結構之前,我們有必要先來了解一下作業系統的記憶體基本結構,這段可不能跳過,它會有助于消化上面的那個圖哦!好先來看圖

作業系統記憶體布局:

那麼jvm在作業系統中如何表示的呢?

作業系統中的jvm

為什麼jvm的記憶體是分布在作業系統的堆中呢??因為作業系統的棧是作業系統管理的,它随時會被回收,是以如果jvm放在棧中,那java的一個null對象就很難确定會被誰回收了,那gc的存在就一點意義都莫有了,而要對棧做到自動釋放也是jvm需要考慮的,是以放在堆中就最合适不過了。

作業系統+jvm的記憶體簡單布局

從上圖中,你有沒有發現什麼規律,jvm的記憶體結構居然和作業系統的結構驚人的一緻,你能不能給他們對号入座?還不能,沒關系,再來看一個圖,我幫你對号入座。看我下面紅色的标注

                     從這個圖,你應該不難發現,原來jvm的設計的模型其實就是作業系統的模型,基于作業系統的角度,jvm就是個該死的java.exe/javaw.exe,也就是一個應用,而基于class檔案來說,jvm就是個作業系統,而jvm的方法區,也就相當于作業系統的硬碟區,是以你知道我為什麼喜歡叫他permanent區嗎,因為這個單詞是永久的意思,也就是永久區,我們的磁盤就是不斷電的永久區嘛,是一樣的意思啊,多好對應啊。而java棧和作業系統棧是一緻的,無論是生長方向還是管理的方式,至于堆嘛,雖然概念上一緻目标也一緻,配置設定記憶體的方式也一直(new,或者malloc等等),但是由于他們的管理方式不同,jvm是gc回收,而作業系統是程式員手動釋放,是以在算法上有很多的差異,gc的回收算法,估計是jvm裡面的經典啊,後面我們也會一點點的學習的,不要着急。

                     有沒有突然自信的感覺?如果你對我的文章有自信,我們再繼續,還是以圖解的方式,我還是那一句,對于概念我絕對有信心讓它在你腦子裡根深蒂固。

                     看下面的圖。

                        将這個圖和上面的圖對比多了什麼?沒錯,多了一個pc寄存器,我為什麼要畫出來,主要是要告訴你,所謂pc寄存器,無論是在虛拟機中還是在我們虛拟機所寄宿的作業系統中功能目的是一緻的,計算機上的pc寄存器是計算機上的硬體,本來就是屬于計算機,(這一點對于學過彙編的同學應該很容易了解,有很多的寄存器eax,esp之類的32位寄存器,jvm裡的寄存器就相當于彙編裡的esp寄存器),計算機用pc寄存器來存放“僞指令”或位址,而相對于虛拟機,pc寄存器它表現為一塊記憶體(一個字長,虛拟機要求字長最小為32位),虛拟機的pc寄存器的功能也是存放僞指令,更确切的說存放的是将要執行指令的位址,它甚至可以是作業系統指令的本地位址,當虛拟機正在執行的方法是一個本地方法的時候,jvm的pc寄存器存儲的值是undefined,是以你現在應該很明确的知道,虛拟機的pc寄存器是用于存放下一條将要執行的指令的位址(位元組碼流)。

                        再對上面的圖擴充,這一次,我們會稍微的深入一點,放心啦,不會很深入,我們的目标是淺顯易懂,好學易記嘛!看下面的圖。

多了什麼?沒錯多了一個classLoader,其實這個圖是要告訴你,當一個classLoder啟動的時候,classLoader的生存地點在jvm中的堆,然後它會去主機硬碟上将A.class裝載到jvm的方法區,方法區中的這個位元組檔案會被虛拟機拿來new A位元組碼(),然後在堆記憶體生成了一個A位元組碼的對象,然後A位元組碼這個記憶體檔案有兩個引用一個指向A的class對象,一個指向加載自己的classLoader,如下圖。

那麼方法區中的位元組碼記憶體塊,除了記錄一個class自己的class對象引用和一個加載自己的ClassLoader引用之外,還記錄了什麼資訊呢??我們還是看圖,然後我會講給你聽,聽過一遍之後一輩子都不會忘記。

你仔細将這個位元組碼和我們的類對應,是不是和一個基本的java類驚人的一緻?下面你看我貼出的一個類的基本結構。

package test;import java.io.Serializable;public final class ClassStruct extends Object implements Serializable {//1.類資訊
 //2.對象字段資訊
 private String name;
 private int id;
 
 //4.常量池
 public final int CONST_INT=0;
    public final String CONST_STR="CONST_STR";
    
    //5.類變量區
    public static String static_str="static_str";
    
 
 //3.方法資訊
 public static final String getStatic_str ()throws Exception{
  return ClassStruct.static_str;
 }}      

你将上面的代碼注解和上面的那個位元組碼碼記憶體塊按标号對應一下,有沒有發現,其實記憶體的位元組碼塊就是完整的把你整個類裝到了記憶體而已。

是以各個資訊段記錄的資訊可以從我們的類結構中得到,不需要你硬背,你認真的看過我下面的描述一遍估計就不可能會忘記了:

       1.類資訊:修飾符(public final)

                        是類還是接口(class,interface)

                        類的全限定名(Test/ClassStruct.class)

                        直接父類的全限定名(java/lang/Object.class)

                        直接父接口的權限定名數組(java/io/Serializable)

      也就是 public final class ClassStruct extends Object implements Serializable這段描述的資訊提取

       2.字段資訊:修飾符(pirvate)

                            字段類型(java/lang/String.class)

                            字段名(name)

        也就是類似private String name;這段描述資訊的提取

       3.方法資訊:修飾符(public static final)

                          方法傳回值(java/lang/String.class)

                          方法名(getStatic_str)

                          參數需要用到的局部變量的大小還有操作數棧大小(操作數棧我們後面會講)

                          方法體的位元組碼(就是花括号裡的内容)

                          異常表(throws Exception)

       也就是對方法public static final String getStatic_str ()throws Exception的位元組碼的提取

       4.常量池:

                    4.1.直接常量:

                                   1.1CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;

                                   1.2CONSTANT_String_info字元串直接常量池   public final String CONST_STR="CONST_STR";

                                   1.3CONSTANT_DOUBLE_INFO浮點型直接常量池

                                   等等各種基本資料類型基礎常量池(待會我們會反編譯一個類,來檢視它的常量池等。)

                     4.2.方法名、方法描述符、類名、字段名,字段描述符的符号引用

            也就是是以編譯器能夠被确定,能夠被快速查找的内容都存放在這裡,它像數組一樣通過索引通路,就是專門用來做查找的。

            編譯時就能确定數值的常量類型都會複制它的所有常量到自己的常量池中,或者嵌入到它的位元組碼流中。作為常量池或者位元組碼流的一部分,編譯時常量儲存在方法區中,就和一般的類變量一樣。但是當一般的類變量作為他們的類型的一部分資料而儲存的時候,編譯時常量作為使用它們的類型的一部分而儲存

      5.類變量:

                  就是靜态字段( public static String static_str="static_str";)

                  虛拟機在使用某個類之前,必須在方法區為這些類變量配置設定空間。

      6.一個到classLoader的引用,通過this.getClass().getClassLoader()來取得為什麼要先經過class呢?思考一下,然後看第七點的解釋,再回來思考

      7.一個到class對象的引用,這個對象存儲了所有這個位元組碼記憶體塊的相關資訊。是以你能夠看到的區域,比如:類資訊,你可以通過this.getClass().getName()取得

          所有的方法資訊,可以通過this.getClass().getDeclaredMethods(),字段資訊可以通過this.getClass().getDeclaredFields(),等等,是以在位元組碼中你想得到的,調用的,通過class這個引用基本都能夠幫你完成。因為他就是位元組碼在記憶體塊在堆中的一個對象

      8.方法表,如果學習c++的人應該都知道c++的對象記憶體模型有一個叫虛表的東西,java本來的名字就叫c++- -,它的方法表其實說白了就是c++的虛表,它的内容就是這個類的所有執行個體可能被調用的所有執行個體方法的直接引用。也是為了動态綁定的快速定位而做的一個類似緩存的查找表,它以數組的形式存在于記憶體中。不過這個表不是必須存在的,取決于虛拟機的設計者,以及運作虛拟機的機器是否有足夠的記憶體

--------------------------------------------------------------------------------------忽略下面虛線間的廢話-------------------------------------------------------------------------------------------------------------------------

好了,還剩這麼多沒講過。不過不要急,我一向提倡,學到哪裡講到哪裡,看到哪裡。是以沒有學到的概念,讓他随風去。

但是我還是會來串一下思路滴:

                 首先,當一個程式啟動之前,它的class會被類裝載器裝入方法區(不好聽,其實這個區我喜歡叫做Permanent區),執行引擎讀取方法區的位元組碼自适應解析,邊解析就邊運作(其中一種方式),然後pc寄存器指向了main函數所在位置,虛拟機開始為main函數在java棧中預留一個棧幀(每個方法都對應一個棧幀),然後開始跑main函數,main函數裡的代碼被執行引擎映射成本地作業系統裡相應的實作,然後調用本地方法接口,本地方法運作的時候,操縱系統會為本地方法配置設定本地方法棧,用來儲存一些臨時變量,然後運作本地方法,調用作業系統APIi等等。         

好吧,你聽暈了,我知道,先記住這段話的位置,等某年某月我提醒你回來看,你就煥然大悟了,現在你隻需要走馬觀花咯!!!

 好了,這一節的内容實在夠多了,是以我打算把它拆解一下,剩下的内容放到下一節,下一節我們會來學習虛拟機的堆棧,和堆。

繼續閱讀