JVM深入理解(一)

一. JVM的物理結構


JVM內存結構主要包括兩個子系統和兩個組件。兩個子系統分別是Classloader子系統和Executionengine(執行引擎)子系統;兩個組件分別是Runtimedataarea(運行時數據區域/內存空間)組件和Nativeinterface(本地接口)組件。

Classloader子系統的作用:根據給定的全限定名類名(如java.lang.Object)來裝載class文件的內容到Runtimedataarea中的methodarea(方法區域)。Java程序員可以extendsjava.lang.ClassLoader類來寫自己的Classloader。

Executionengine子系統的作用:執行classes中的指令。任何JVMspecification實現(JDK)的核心都是Executionengine,不同的JDK例如Sun的JDK和IBM的JDK好壞主要就取決於他們各自實現的Executionengine的好壞。

Nativeinterface組件:與nativelibraries交互,是其它編程語言交互的接口。當調用native方法的時候,就進入了一個全新的並且不再受虛擬機限制的世界,所以也很容易出現JVM無法控制的nativeheapOutOfMemory。

RuntimeDataArea組件:

這就是我們常說的JVM的內存了。它主要分爲五個部分——

1、Heap(堆):一個Java虛擬實例中只存在一個堆空間。

2、MethodArea(方法區域):被裝載的class的信息存儲在Methodarea的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的class文件,然後讀入這個class文件內容並把它傳輸到虛擬機中。

3、JavaStack(java的棧):虛擬機只會直接對Javastack執行兩種操作:以幀爲單位的壓棧或出棧。

4、ProgramCounter(程序計數器):每一個線程都有它自己的PC寄存器,也是該線程啓動時創建的。PC寄存器的內容總是指向下一條將被執行指令的餓地址,這裏的地址可以是一個本地指針,也可以是在方法區中相對應於該方法起始指令的偏移量。

5、Nativemethodstack(本地方法棧):保存native方法進入區域的地址。


二. java類的加載與運行

class文件的構成:

  • 結構信息。包括class文件格式版本號及各部分的數量與大小的信息。
  • 元數據。對應於Java源碼中聲明與常量的信息。包含類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池。
  • 方法信息。對應Java源碼中語句和表達式對應的信息。包含字節碼、異常處理器表、求值棧與局部變量區大小、求值棧的類型記錄、調試符號信息。

    過去的誤解:以爲方法區只保存class中定義的方法,實際遠遠不夠。
方法區是系統分配的一個內存邏輯區域,是用來存儲類型信息的(類型信息可理解爲類的描述信息)。方法區主要有以下幾個特點:
一、方法區是線程安全的。由於所有的線程都共享方法區,所以,方法區裏的數據訪問必須被設計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區中的同一個類,而這個類還沒有被裝入JVM,那麼只允許一個線程去裝載它,而其它線程必須等待 

二、方法區的大小不必是固定的,JVM可根據應用需要動態調整。同時,方法區也不一定是連續的,方法區可以在一個堆(甚至是JVM自己的堆)中自由分配。 

三、方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集 。

四、方法區裏存放的是哪些內容? 
    方法區裏存的都是類型信息,也就是類的信息,而類的信息又包括以下內容: 
    類的全限定名(類的全路徑名) 
    類的直接超類的全限定名(如果這個類是Object,則它沒有超類) 
    這個類是類型(類)還是接口 
    類的訪問修飾符,如public、abstract、final等 
    所有的直接接口全限定名的有序列表(假如它實現了多個接口) 

    字段、方法信息、類變量信息(靜態變量)    裝載該類的裝載器的引用(classLoader)、類型引用(class) 

    常量池

     Java中的常量池,實際上分爲兩種形態:靜態常量池運行時常量池

     所謂靜態常量池,即*.class文件中的常量池,class文件中的常量池不僅僅包含字符串(數字)字面量,還包含類、方法的信息,佔用class文件絕大部分空間。

     而運行時常量池,則是jvm虛擬機在完成類裝載操作後,將class文件中的常量池載入到內存中,並保存在方法區中,我們常說的常量池,就是指方法區中的運行時常量池。

對於運行時常量池,用字符串常量的例子解釋會比較好理解:
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;

System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

    s1 == s2這個非常好理解,s1、s2在賦值時,均使用的字符串字面量,說白話點,就是直接把字符串寫死,在編譯期間,這種字面量會直接放入class文件的常量池中,從而實現複用,載入運行時常量池後,s1、s2指向的是同一個內存地址,所以相等。

    s1 == s3這個地方有個坑,s3雖然是動態拼接出來的字符串,但是所有參與拼接的部分都是已知的字面量,在編譯期間,這種拼接會被優化,編譯器直接幫你拼好,因此String s3 = "Hel" + "lo";在class文件中被優化成String s3 = "Hello";,所以s1 == s3成立。

    s1 == s4當然不相等,s4雖然也是拼接出來的,但new String("lo")這部分不是已知字面量,是一個不可預料的部分,編譯器不會優化,必須等到運行時纔可以確定結果,結合字符串不變定理,鬼知道s4被分配到哪去了,所以地址肯定不同。

    s1 == s9也不相等,道理差不多,雖然s7、s8在賦值的時候使用的字符串字面量,但是拼接成s9的時候,s7、s8作爲兩個變量,都是不可預料的,編譯器畢竟是編譯器,不可能當解釋器用,所以不做優化,等到運行時,s7、s8拼接成的新字符串,在堆中地址不確定,不可能與方法區常量池中的s1地址相同。

     s4 == s5已經不用解釋了,二者都在堆中,但地址不同。

     s1 == s6這兩個相等完全歸功於intern方法,s5在堆中,內容爲Hello ,intern方法會嘗試將Hello字符串添加到常量池中,並返回其在常量池中的地址,因爲常量池中已經有了Hello字符串,所以intern方法直接返回地址;而s1在編譯期就已經指向常量池了,因此s1和s6指向同一地址,相等。

可以得出三個結論:

可以從編譯器的角度去理解常量池(變量在編譯期間是沒有字面值的,例子中的(s1 == s9))。

運行時常量池中的常量,基本來源於各個class文件中的常量池。

程序運行時jvm不會自動添加常量到常量池,除非手動向常量池中添加常量(比如調用intern方法)。






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