JVM載入class檔案的原理機制簡單總結
阿新 • • 發佈:2019-01-05
Java中的所有類,必須被裝載到jvm中才能執行,這個裝載工作是由jvm中的類裝載器完成的,類裝載器所做的工作實質是把類檔案從硬碟讀取到記憶體中
java中的類大致分為三種:
1).系統類
2).擴充套件類
3).由程式設計師自定義的類
類裝載方式,有兩種
1).隱式裝載, 程式在執行過程中當碰到通過new 等方式生成物件時,隱式呼叫類裝載器載入對應的類到jvm中
2).顯式裝載, 通過class.forname()等方法,顯式載入需要的類
類載入的動態性體現
一個應用程式總是由n多個類組成,Java程式啟動時,並不是一次把所有的類全部載入後再
執行,它總是先把保證程式執行的基礎類一次性載入到jvm中,其它類等到jvm用到的時候再載入,這樣的好處是節省了記憶體的開銷,因為java最早就是為嵌入式系統而設計的,記憶體寶貴,這是一種可以理解的機制,而用到時再載入這也是java動態性的一種體現
java類裝載器
Java中的類裝載器實質上也是類,功能是把類載入jvm中,值得注意的是jvm的類裝載器並不是一個,而是三個,層次結構如下:
Bootstrap Loader - 負責載入系統類
|
- - ExtClassLoader - 負責載入擴充套件類
|
- - AppClassLoader - 負責載入應用類
為什麼要有三個類載入器,一方面是分工,各自負責各自的區塊,另一方面為了實現委託模型,下面會談到該模型
1) Bootstrap類載入器 – JRE/lib/rt.jar
2) Extension類載入器 – JRE/lib/ext或者java.ext.dirs指向的目錄
3) Application類載入器 – CLASSPATH環境變數, 由-classpath或-cp選項定義,或者是JAR中的Manifest的classpath屬性定義.
類載入器之間是如何協調工作的
類載入器的工作原理基於三個機制:委託、可見性和單一性
委託機制
當一個類載入和初始化的時候,類僅在有需要載入的時候被載入。假設你有一個應用需要的類叫作Abc.class,首先載入這個類的請求由 Application類載入器委託給它的父類載入器Extension類載入器,然後再委託給Bootstrap類載入器。Bootstrap類載入器 會先看看rt.jar中有沒有這個類,因為並沒有這個類,所以這個請求由回到Extension類載入器,它會檢視jre/lib/ext目錄下有沒有這 個類,如果這個類被Extension類載入器找到了,那麼它將被載入,而Application類載入器不會載入這個類;而如果這個類沒有被 Extension類載入器找到,那麼再由Application類載入器從classpath中尋找。記住classpath定義的是類檔案的載入目 錄,而PATH是定義的是可執行程式如javac,java等的執行路徑。
可見性機制
可見性的原理是子類的載入器可以看見所有的父類載入器載入的類,而父類載入器看不到子類載入器載入的類。
單一性機制
根據這個機制,父載入器載入過的類不能被子載入器載入第二次。雖然重寫違反委託和單一性機制的類載入器是可能的,但這樣做並不可取。
執行緒上下文類載入器(context class loader)是從 JDK 1.2 開始引入的。類 Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設定執行緒的上下文類載入器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設定的話,執行緒將繼承其父執行緒的上下文類載入器。Java 應用執行的初始執行緒的上下文類載入器是系統類載入器。線上程中執行的程式碼可以通過此類載入器來載入類和資源。
前面提到的類載入器的代理模式並不能解決 Java 應用開發中會遇到的類載入器的全部問題。Java 提供了很多服務提供者介面(Service Provider Interface,SPI),允許第三方為這些介面提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的介面由 Java 核心庫來提供,如 JAXP 的 SPI 介面定義包含在 javax.xml.parsers包中。這些 SPI 的實現程式碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 介面中的程式碼經常需要載入具體的實現類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的 DocumentBuilderFactory的例項。這裡的例項的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在於,SPI 的介面是 Java 核心庫的一部分,是由引導類載入器來載入的;SPI 實現的 Java 類一般是由系統類載入器來載入的。引導類載入器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類載入器,因為它是系統類載入器的祖先類載入器。也就是說,類載入器的代理模式無法解決這個問題。
執行緒上下文類載入器正好解決了這個問題。如果不做任何的設定,Java 應用的執行緒的上下文類載入器預設就是系統上下文類載入器。在 SPI 介面的程式碼中使用執行緒上下文類載入器,就可以成功的載入到 SPI 實現的類。執行緒上下文類載入器在很多 SPI 的實現中都會用到。
類的載入過程
1) 裝載:查詢並載入類的二進位制資料;
2)連結:
驗證:確保被載入類的正確性;
準備:為類的靜態變數分配記憶體,並將其初始化為預設值;
解析:把類中的符號引用轉換為直接引用;
3)初始化:為類的靜態變數賦予正確的初始值;
那為什麼我要有驗證這一步驟呢?首先如果由編譯器生成的class檔案,它肯定是符合JVM位元組碼格式的,但是萬一有高手自己寫一個class檔案,讓JVM載入並執行,用於惡意用途,就不妙了,因此這個class檔案要先過驗證這一關,不符合的話不會讓它繼續執行的,也是為了安全考慮吧。
準備階段和初始化階段看似有點牟盾,其實是不牟盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先位元組碼檔案被載入到記憶體後,先進行連結的驗證這一步驟,驗證通過後準備階段,給a分配記憶體,因為變數a是static的,所以此時a等於int型別的預設初始值0,即a=0,然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。
類的初始化
類什麼時候才被初始化:
1)建立類的例項,也就是new一個物件
2)訪問某個類或介面的靜態變數,或者對該靜態變數賦值
3)呼叫類的靜態方法
4)反射(Class.forName("com.lyj.load"))
5)初始化一個類的子類(會首先初始化子類的父類)
6)JVM啟動時標明的啟動類,即檔名和類名相同的那個類
只有這6中情況才會導致類的類的初始化。
類的初始化步驟:
1)如果這個類還沒有被載入和連結,那先進行載入和連結
2)假如這個類存在直接父類,並且這個類還沒有被初始化(注意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類(不適用於介面)
3)加入類中存在初始化語句(如static變數和static塊),那就依次執行這些初始化語句。
java中的類大致分為三種:
1).系統類
2).擴充套件類
3).由程式設計師自定義的類
類裝載方式,有兩種
1).隱式裝載, 程式在執行過程中當碰到通過new 等方式生成物件時,隱式呼叫類裝載器載入對應的類到jvm中
2).顯式裝載, 通過class.forname()等方法,顯式載入需要的類
類載入的動態性體現
一個應用程式總是由n多個類組成,Java程式啟動時,並不是一次把所有的類全部載入後再
執行,它總是先把保證程式執行的基礎類一次性載入到jvm中,其它類等到jvm用到的時候再載入,這樣的好處是節省了記憶體的開銷,因為java最早就是為嵌入式系統而設計的,記憶體寶貴,這是一種可以理解的機制,而用到時再載入這也是java動態性的一種體現
java類裝載器
Java中的類裝載器實質上也是類,功能是把類載入jvm中,值得注意的是jvm的類裝載器並不是一個,而是三個,層次結構如下:
Bootstrap Loader - 負責載入系統類
|
- - ExtClassLoader - 負責載入擴充套件類
|
- - AppClassLoader - 負責載入應用類
為什麼要有三個類載入器,一方面是分工,各自負責各自的區塊,另一方面為了實現委託模型,下面會談到該模型
1) Bootstrap類載入器 – JRE/lib/rt.jar
2) Extension類載入器 – JRE/lib/ext或者java.ext.dirs指向的目錄
3) Application類載入器 – CLASSPATH環境變數, 由-classpath或-cp選項定義,或者是JAR中的Manifest的classpath屬性定義.
類載入器之間是如何協調工作的
類載入器的工作原理基於三個機制:委託、可見性和單一性
委託機制
當一個類載入和初始化的時候,類僅在有需要載入的時候被載入。假設你有一個應用需要的類叫作Abc.class,首先載入這個類的請求由 Application類載入器委託給它的父類載入器Extension類載入器,然後再委託給Bootstrap類載入器。Bootstrap類載入器 會先看看rt.jar中有沒有這個類,因為並沒有這個類,所以這個請求由回到Extension類載入器,它會檢視jre/lib/ext目錄下有沒有這 個類,如果這個類被Extension類載入器找到了,那麼它將被載入,而Application類載入器不會載入這個類;而如果這個類沒有被 Extension類載入器找到,那麼再由Application類載入器從classpath中尋找。記住classpath定義的是類檔案的載入目 錄,而PATH是定義的是可執行程式如javac,java等的執行路徑。
可見性機制
可見性的原理是子類的載入器可以看見所有的父類載入器載入的類,而父類載入器看不到子類載入器載入的類。
單一性機制
根據這個機制,父載入器載入過的類不能被子載入器載入第二次。雖然重寫違反委託和單一性機制的類載入器是可能的,但這樣做並不可取。
執行緒上下文類載入器(context class loader)是從 JDK 1.2 開始引入的。類 Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設定執行緒的上下文類載入器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設定的話,執行緒將繼承其父執行緒的上下文類載入器。Java 應用執行的初始執行緒的上下文類載入器是系統類載入器。線上程中執行的程式碼可以通過此類載入器來載入類和資源。
前面提到的類載入器的代理模式並不能解決 Java 應用開發中會遇到的類載入器的全部問題。Java 提供了很多服務提供者介面(Service Provider Interface,SPI),允許第三方為這些介面提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的介面由 Java 核心庫來提供,如 JAXP 的 SPI 介面定義包含在 javax.xml.parsers包中。這些 SPI 的實現程式碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 介面中的程式碼經常需要載入具體的實現類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的 DocumentBuilderFactory的例項。這裡的例項的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在於,SPI 的介面是 Java 核心庫的一部分,是由引導類載入器來載入的;SPI 實現的 Java 類一般是由系統類載入器來載入的。引導類載入器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類載入器,因為它是系統類載入器的祖先類載入器。也就是說,類載入器的代理模式無法解決這個問題。
執行緒上下文類載入器正好解決了這個問題。如果不做任何的設定,Java 應用的執行緒的上下文類載入器預設就是系統上下文類載入器。在 SPI 介面的程式碼中使用執行緒上下文類載入器,就可以成功的載入到 SPI 實現的類。執行緒上下文類載入器在很多 SPI 的實現中都會用到。
類的載入過程
JVM將類載入過程分為三個步驟:裝載(Load),連結(Link)和初始化(Initialize)連結又分為三個步驟,如下圖所示:
1) 裝載:查詢並載入類的二進位制資料;
2)連結:
驗證:確保被載入類的正確性;
準備:為類的靜態變數分配記憶體,並將其初始化為預設值;
解析:把類中的符號引用轉換為直接引用;
3)初始化:為類的靜態變數賦予正確的初始值;
那為什麼我要有驗證這一步驟呢?首先如果由編譯器生成的class檔案,它肯定是符合JVM位元組碼格式的,但是萬一有高手自己寫一個class檔案,讓JVM載入並執行,用於惡意用途,就不妙了,因此這個class檔案要先過驗證這一關,不符合的話不會讓它繼續執行的,也是為了安全考慮吧。
準備階段和初始化階段看似有點牟盾,其實是不牟盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先位元組碼檔案被載入到記憶體後,先進行連結的驗證這一步驟,驗證通過後準備階段,給a分配記憶體,因為變數a是static的,所以此時a等於int型別的預設初始值0,即a=0,然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。
類的初始化
類什麼時候才被初始化:
1)建立類的例項,也就是new一個物件
2)訪問某個類或介面的靜態變數,或者對該靜態變數賦值
3)呼叫類的靜態方法
4)反射(Class.forName("com.lyj.load"))
5)初始化一個類的子類(會首先初始化子類的父類)
6)JVM啟動時標明的啟動類,即檔名和類名相同的那個類
只有這6中情況才會導致類的類的初始化。
類的初始化步驟:
1)如果這個類還沒有被載入和連結,那先進行載入和連結
2)假如這個類存在直接父類,並且這個類還沒有被初始化(注意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類(不適用於介面)
3)加入類中存在初始化語句(如static變數和static塊),那就依次執行這些初始化語句。