JVM基礎和調優(二)

主要講述java虛擬機的內存體系結構java

瞭解了JVM 的一些基礎以後,咱們來看看java虛擬機內存的體系結構,這個是理解JVM垃圾收集算法的前提,理解了內存結構咱們纔可以針對不一樣的部分根據咱們的程序進行優化。前面已經說明了,java的堆和棧,可是隻是局部的說了一下,沒有在java內存體系中說明。c++

這一節,主要來學習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 c#

第三步:打開任務管理器-進程 數組

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

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

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

好了,我已經圈出來了,有兩個javaw.exe,爲何會有兩個?由於咱們剛纔運行了兩次run application。這裏我是要告訴你,一個java的application對應了一個java.exe/javaw.exe(java.exe和javaw.exe你能夠把它當作java的虛擬機,一個有窗口界面一個沒有)。你運行幾個application就有幾個java.exe/javaw.exe。或者更加具體的說,你運行了幾個main函數就啓動了幾個java應用,同時也啓動了幾個java的虛擬機。

------main 方法,程序的入口

知識點1總結:java的虛擬機至關於咱們的一個java類,而java虛擬機實例,至關咱們new一個java類,不過java虛擬機不是經過new這個關鍵字而是經過java.exe或者javaw.exe來啓動一個虛擬機實例。

知識點2:jvm的生命週期

基本上學習一種容器(更具體的說咱們在學習servlet的時候),咱們都要學習它的生命週期。那麼jvm的生命週期如何

第一步:測試代碼:

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何時消失

[java] view plaincopy

  1. 0 睡了0秒  
  2. 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秒  
  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虛擬機實例才結束生命。

------JDK 提供的方法 RunTime  addShutDownHot 可以在虛擬機退出的時候執行。

知識點三: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.  

你將上面的代碼註解和上面的那個字節碼碼內存塊按標號對應一下,有沒有發現,其實內存的字節碼塊就是完整的把你整個類裝到了內存而已。

因此各個信息段記錄的信息能夠從咱們的類結構中獲得,不須要你硬背,你認真的看過我下面的描述一遍估計就不可能會忘記了

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

轉載文章:http://blog.csdn.net/yfqnihao 有改動。

相關文章
相關標籤/搜索