1. 程式人生 > >【深入理解JVM】學習筆記——-1、JVM基本結構

【深入理解JVM】學習筆記——-1、JVM基本結構

轉載自:https://blog.csdn.net/singit/article/details/54920387?utm_source=blogkpcl11

 

什麼是jvm?JVM的基本結構,

也就是概述。說是概述,內容很多,而且概念量也很大,

不過關於概念方面,你不用擔心,我完全有信心,讓概念在你的腦子裡變成圖形,

所以只要你有耐心,仔細,認真,併發揮你的想象力,這一章之後你會充滿自信。

當然,不是說看完本章,就對jvm瞭解了,jvm要學習的知識實在是非常的多。

在你看完本節之後,後續我們還會來學jvm的細節,但

是如果你在學習完本節的前提下去學習,再學習其他jvm的細節會事半功倍。

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

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

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

                       第一步:先來寫一個類:  

[java]  view plain  copy      
  1. package test;  
  2.   
  3. public class JVMTestForJava {  
  4.     public static void main(String[] args) throws InterruptedException {  
  5.         Thread.sleep(10000000);  
  6. }  
  7. }  

                       第二步: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我程式碼

[java]  view plain  copy      
  1. package test;  
  2.   
  3. public class JVMTestLife {  
  4.     public static void main(String[] args) {  
  5.         new Thread(new Runnable() {  
  6.             @Override  
  7.             public void run() {  
  8.                 for(int i=0;i<5;i++){  
  9.                     try {  
  10.                         Thread.currentThread().sleep(i*10000);  
  11.                         System.out.println("睡了"+i*10+"秒");  
  12.                     } catch (InterruptedException e) {  
  13.                         System.out.println("幹嘛吵醒我");  
  14.                     }  
  15.                 }  
  16.             }  
  17.         }).start();   
  18.           
  19.         for(int i=0;i<50;i++){  
  20.                 System.out.print(i);  
  21.         }  
  22.     }  
  23. }  

                      第二步:ecplise裡run application

                      第三步:開啟工作管理員-程序,看到一個javaw.exe的虛擬機器在跑


                    第四步:檢視控制檯輸出,並觀察工作管理員中的javaw.exe什麼時候消失

[java]  view plain  copy      
  1. 0 睡了0秒  
  2. 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秒  
  3. 睡了20秒  
  4. 睡了30秒  
  5. 睡了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類驚人的一致?下面你看我貼出的一個類的基本結構。

[java]  view plain  copy      
  1. package test;import java.io.Serializable;public final class ClassStruct extends Object implements Serializable {//1.類資訊  
  2.  //2.物件欄位資訊  
  3.  private String name;  
  4.  private int id;  
  5.    
  6.  //4.常量池  
  7.  public final int CONST_INT=0;  
  8.     public final String CONST_STR="CONST_STR";  
  9.       
  10.     //5.類變數區  
  11.     public static String static_str="static_str";  
  12.       
  13.    
  14.  //3.方法資訊  
  15.  public static final String getStatic_str ()throws Exception{  
  16.   return ClassStruct.static_str;  
  17.  }}  

你將上面的程式碼註解和上面的那個位元組碼碼記憶體塊按標號對應一下,有沒有發現,其實記憶體的位元組碼塊就是完整的把你整個類裝到了記憶體而已。

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

       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等等。         

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

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