JVM內存模型
Java代碼是運行在Java虛擬機(JVM)上的,Java虛擬機通過解釋執行(解釋器)或編譯執行(編譯器)來完成。
Java內存模型分為5個部分:方法區(Method Area),Java堆(Heap),Java棧(VM Stack),本地方法棧(Native Method Stack),程序計數器(PC 寄存器)
(圖片來源:http://gityuan.com/images/jvm/jvm_memory_1.png)
線程共享區:
方法區(Method Area):方法區是各個線程共享的區域,存放類信息,常量,靜態常量,編譯器編譯后的代碼等信息。
Java堆(Heap):Java堆也是線程共享區域,類的實例存放在這里,一個系統會產生很多Java實例,因此Java堆的空間是最大的,如果Java堆的空間不足,就會拋出OutOfMemoryError異常。
線程私有區:
Java棧(VM Stack):線程私有區域,生命周期與線程相同,一個線程對應一個Java棧,每執行一個方法就會向棧里壓一個元素,這個元素叫“棧幀”,棧幀中包含了方法中保存了該方法調用的參數、局部變量和返回地址等信息,如果棧空間不足了就會拋出StackOverflowError異常。
本地方法棧(Native Method Stack):和Java棧類似,本地方法棧是用來執行本地方法的,存放的方法調用本地方法接口,最終調用本地方法庫,實現與操作系統,硬件交互的目的。
程序計數器:這里對應的類以及加載,實例對象,方法,靜態變量去了該去的地方,那么問題來了,程序該怎么執行,哪個方法先執行,哪個方法后執行,這些指令執行的順序就是PC寄存器在管,它的作用就是控制程序指令的執行順序。
類加載機制
編寫的java代碼會通過編譯器編譯成字節編碼的.class文件,再把字節編碼加載到JVM中,映射到內存的各個區域中,程序就可以在內存中運行了。
類加載流程
(圖片來源:https://images2017.cnblogs.com/blog/352511/201708/352511-20170825174319746-900347526.png)
1,加載
加載是類裝載的第一步,內存中生成一個代表這個類的java.lang.class對象,通過class文件的路徑讀取到二進制流,並解析二進制里的元數據(類型,常量等),作為方法區這個類的各種數據量的入口;這里的不一定從class文件獲取,這里既可以從ZIP包(jar,war)包中獲取,也在運行時動態生成(jsp轉換成class文件,動態代理生成)。
2,連接
連接又可分為驗證,准備,解析。
2.1,驗證
驗證主要是判斷class文件的合法性,對版本號進行驗證(例如如果使用java1.8編譯后的class文件要再java1.6虛擬機上運行),還會對元數據,字節編碼等進行驗證,確保class文件里的字節流信息符合當前虛擬機的要求,不會危害虛擬機的安全。
2.2,准備
准備主要是分配內存,為變量分配初始值,即在方法區中分配這些變量所使用的內存空間,例如:
public static int i = 1;
在准備階段i的值會被初始化為0,后面的類的初始化階段才會賦值為1;
public static final int i = 1;
對應常量(static final)i,在准備階段就會被賦值1;
2.3,解析
解析就是把代碼中的符號引用替換為直接引用;例如某個類繼承了java.lang.Object,原來的符號引用記錄的是“java.lang.Object”,並不是java.lang,Object對象,直接引用就是找出對應的java.lang.Object對應的內存地址,建立直接引用關系;
3,初始化
初始化的過程包括執行類構造器方法,static變量賦值語句,static{}代碼塊,如果是一個子類進行初始化會先對其父類進行初始化,保證其父類在子類之前進行初始化;所以其實在java中初始化一個類,那么必然是先初始化java.lang.Object,因為所有的java類都繼承自java.lang.Object。
以下幾種情況不會執行類初始化:
- 通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
- 定義對象數組,不會觸發該類的初始化。
- 常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。
- 通過類名獲取Class對象,不會觸發類的初始化。
- 通過Class.forName加載指定類時,如果指定參數initialize為false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
- 通過ClassLoader默認的loadClass方法,也不會觸發初始化動作。
類加載器
在JVM中有三中類加載器,BootStrap Classloader(啟動Classloader)、Extension Classloader(擴展Classloader)和APP Classloader(應用Classloader);
BootStrap ClassLoader主要加載JVM自身需要的類,這個加載器由C++編寫是虛擬機的一部分,負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
Extension Classloader是sun.misc.Launcher中的內部類ExtClassLoader,負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。
APP ClassLoader是sun.misc.Launcher中的內部類AppClassLoader,負責加載用戶路徑上的類庫。
用戶也可以通過繼承ClassLoader實現自己的類加載器。
雙親委派模式
當一個類加載器接收到類加載的任務時,會首先交給其父類加載器去加載,只有當父類加載器無法加載是其才會自己加載。其好處是可以避免一個類被重復加載。
即使兩個類來源於相同的class文件,如果使用的類加載器不同,加載后的對象時完全不同的,這個不同反應在對象的 equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用 instanceof 關鍵字對對象所屬關系的判定結果。
雙親委派模式的問題
頂層ClassLoader,無法加載底層ClassLoader的類
Java框架(rt.jar)如何加載應用的類?
比如:javax.xml.parsers包中定義了xml解析的類接口
Service Provider Interface SPI 位於rt.jar
即接口在啟動ClassLoader中。
而SPI的實現類,在AppLoader。
這樣就無法用BootstrapClassLoader去加載SPI的實現類。
解決
JDK中提供了一個方法:
1: Thread. setContextClassLoader()
用以解決頂層ClassLoader無法訪問底層ClassLoader的類的問題;
基本思想是,在頂層ClassLoader中,傳入底層ClassLoader的實例。