java高級架構——JVM內存區域模型和加載過程

JVM內存區域模型

java高級架構——JVM內存區域模型和加載過程

1. 方法區

也稱 " 永久代” 、“非堆”, 它用於存儲虛擬機加載的類信息、常量、靜態變量、是各個線程共享的內存區域。默認最小值為 16MB ,最大值為 64MB ,可以通過 -XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。

運行時常量池:是方法區的一部分, Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯器生成的各種符號引用,這部分內容將在類加載後放到方法區的運行時常量池中。

2. 虛擬機棧

描述的是 java 方法執行的內存模型:每個方法被執行的時候 都會創建一個“棧幀”用於存儲局部變量表 ( 包括參數 ) 、操作棧、方法出口等信息。每個方法被調用到執行完的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。聲明週期與線程相同,是線程私有的。

局部變量表存放了編譯器可知的各種基本數據類型 (boolean 、 byte 、 char 、 short 、 int 、 float 、 long 、 double) 、對象引用 ( 引用指針,並非對象本身 ) ,其中 64 位長度的 long 和 double 類型的數據會佔用 2 個局部變量的空間,其餘數據類型只佔 1 個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的局部變量是完全確定的,在運行期間棧幀不會改變局部變量表的大小空間。

3. 本地方法棧

與虛擬機棧基本類似,區別在於虛擬機棧為虛擬機執行的 java 方法服務,而本地方法棧則是為 Native 方法服務。

4. 堆

也叫做 java 堆、 GC 堆是 java 虛擬機所管理的內存中最大的一塊內存區域,也是被各個線程共享的內存區域,在 JVM 啟動時創建。該內存區域存放了對象實例及數組 ( 所有 new 的對象 ) 。其大小通過 -Xms( 最小值 ) 和 -Xmx( 最大值 ) 參數設置, -Xms 為 JVM 啟動時申請的最小內存,默認為操作系統物理內存的 1/64 但小於 1G , -Xmx 為 JVM 可申請的最大內存,默認為物理內存的 1/4 但小於 1G ,默認當空餘堆內存小於 40% 時, JVM 會增大 Heap 到 -Xmx 指定的大小,可通過 -XX:MinHeapFreeRation= 來指定這個比列;當空餘堆內存大於 70% 時, JVM 會減小 heap的大小到 -Xms 指定的大小,可通過 XX:MaxHeapFreeRation= 來指定這個比列,對於運行系統,為避免在運行時頻繁調整 Heap 的大小,通常 -Xms 與 -Xmx 的值設成一樣。

由於現在收集器都是採用分代收集算法,堆被劃分為新生代和老年代。新生代主要存儲新創建的對象和尚未進入老年代的對象。老年代存儲經過多次新生代 GC(Minor GC) 任然存活的對象。

新生代:

程序新創建的對象都是從新生代分配內存,新生代由 Eden Space 和兩塊相同大小的 Survivor Space( 通常又稱 S0 和 S1 或 From 和 To) 構成,可通過 -Xmn 參數來指定新生代的大小,也可以通過 -XX:SurvivorRation 來調整 Eden Space 及 Survivor Space 的大小。

老年代:

用於存放經過多次新生代 GC 任然存活的對象,例如緩存對象,新建的對象也有可能直接進入老年代,主要有兩種情況:① . 大對象,可通過啟動參數設置 -XX:PretenureSizeThreshold=1024( 單位為字節,默認為 0) 來代表超過多大時就不在新生代分配,而是直接在老年代分配。② . 大的數組對象,切數組中無引用外部對象。

老年代所佔的內存大小為 -Xmx 對應的值減去 -Xmn 對應的值。

5. 程序計數器

是最小的一塊內存區域,它的作用是當前線程所執行的字節碼的行號指示器,在虛擬機的模型裡,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、異常處理、線程恢復等基礎功能都需要依賴計數器完成。

JVM加載過程

Java 語言中,類只有被加載到 JVM 中才能運行,當運行指定的 java 程序時, JVM 會將編譯生成的 .class 文件按照一定的規則加載到內存中,並組織成為一個完整的應用程序。類的加載過程是由類加載器完成的(即由 ClassLoader 和它的子類完成),而類加載器本身也是一個類,其實質是將類文件由硬盤加載到內存中。

類的加載方式有兩種:

( 1 )顯式加載

通過調用 class.forName() 方法將所需的類加載到 JVM 中

( 2 )隱式加載

程序在創建新的對象時,隱式地調用類加載器把對應的類加載到 JVM 中

在 java 語言中,類的加載是動態且靈活的,往往一個大的項目包含很多類,而每一個類或接口都對應一個 .class 文件,當程序運行時只需要將需要的類(保證程序運行的基礎類,例如基類)加載到 JVM 中,暫時不需要的類可以先不加載,這樣一方面可以提高運行速度,另一方面也可以降低程序運行時對內存的開銷。而且,每一個類文件都可以看成是動態的加載單元,當項目需要對某個類進行修改時,修改完畢後只需要重新編譯加載被修改的類即可,而不用全部的類都重新進行編譯。

類可以分為三種:系統類、擴展類、自定義類,而 java 根據不同的類提供了不同的類加載器

Bootstrap Loader <== 加載系統類( jre/lib/rt.jar 的類)

ExtClassLoader <== 加載擴展類( jar/lib/etc/*.jar 的類)

AppClassLoader <== 加載應用類( classpath 指定的目錄或 jar 中的類)

具體步驟:

( 1 )首先 java.exe 會找到 JRE ,並且找到位於 JRE 內部的 jvm.dll ,這才是真正的 java 虛擬機,然後加載到動態庫,激活 java 虛擬機。

( 2 )進行初始化操作,結束之後產生 Bootstrap Loader 啟動類加載器

( 3 ) Bootstrap Loader 除了進行一些基本的初始化動作外,最重要的是加載 ExtClassLoader擴展類加載器,並且設定其 Parent 為 null ,也就代表其父加載器為 Bootstrap Loader

( 4 )然後 Bootstrap Loader 再要求加載 Launcher.java 中的 AppClassLoader( 自定義類加載器 ) ,並設定其 Parent 為 ExtClassLoader 實體,這兩個加載器都是以靜態類的形式存在的。

※需要注意的是,其實 parent 是誰跟被誰加載的並沒有直接關係

我們可以測試一下:

package test; public class classloader { public static void main(String[] args) throws Exception{ ClassLoader App = classloader.class.getClassLoader();//class 加載器 System.out.println(App); ClassLoader Ext = App.getParent();// 上一層加載器 System.out.println(Ext); ClassLoader Boot = Ext.getParent();// 根部加載器 System.out.println(Boot); } }

運行結果:

sun.misc.Launcher$AppClassLoader@39579371

sun.misc.Launcher$ExtClassLoader@2490fd20

null

Bootstrap Loader 輸出 null 的原因是它是由 C++ 語言實現的,所以在 java 語言中看不到

程序說明 classloader 這個類是由 AppClassLoader 加載的

類的加載主要有三步:

( 1 )裝載:根據查找路徑找到相應的 class 文件並導入

( 2 )鏈接:檢查 class 文件是否正確 --> 給類中的靜態變量分配存儲空間 --> 將符號引用轉換成直接引用

( 3 )初始化:靜態變量和靜態代碼塊的初始化操作

雙親委託機制:

雙親委託模式也就是一個類加載器請求另一個類加載器來加載類型的過程。

除啟動類加載器以外的每一個類加載器,都有一個“雙親”類加載器 ,在某個特定的類加載器試圖以常用方式加載某個類以前,它會先默認地將這個任務“委派”給它的雙親,請求它的雙親來加載這個類。這個雙親再依次請求它自己的雙親來加載這個類型。這個委派的過程一直向上繼續,直到達到啟動類加載器,通常啟動類加載器是委派鏈中的最後一個類加載器。如果一個類加載器的雙親類加載器有能力來加載這個類型。則這個類加載器返回這個類型。否則,這個類加載器試圖自己來加載這個類。

當一個程序運行時,虛擬機在啟動時實例化了兩個用戶自定義類加載器 : 一個“擴展類加載器” , 一個“自定義類加載器” . 這些類裝載器和啟動類加載器一起聯入一個 Parent-Child 委託鏈中 ,

啟動類加載器在最頂端。

java高級架構——JVM內存區域模型和加載過程


分享到:


相關文章: