JAVA類裝載方式,有兩種:
1.隱式裝載, 程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中。 2.顯式裝載, 通過class.forname()等方法,顯式加載需要的類
類加載的動態性體現:
一個應用程序總是由n多個類組成,Java程序啟動時,並不是一次把所有的類全部加載后再運行,它總是先把保證程序運行的基礎類一次性加載到jvm中,其它類等到jvm用到的時候再加載,這樣的好處是節省了內存的開銷,因為java最早就是為嵌入式系統而設計的,內存寶貴,這是一種可以理解的機制,而用到時再加載這也是java動態性的一種體現
java類裝載器
JDK 默認提供了如下幾種ClassLoader
Bootstrp loader
Bootstrp加載器是用C++語言寫的,它是在Java虛擬機啟動后初始化的,它主要負責加載%JAVA_HOME%/jre/lib
,-Xbootclasspath
參數指定的路徑以及%JAVA_HOME%/jre/classes
中的類。
ExtClassLoader
Bootstrp loader加載ExtClassLoader,並且將ExtClassLoader的父加載器設置為Bootstrp loader.ExtClassLoader是用Java寫的,具體來說就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext
,此路徑下的所有classes目錄以及java.ext.dirs
系統變量指定的路徑中類庫。AppClassLoader
Bootstrp loader加載完ExtClassLoader后,就會加載AppClassLoader,並且將AppClassLoader的父加載器指定為 ExtClassLoader。AppClassLoader也是用Java寫成的,它的實現類是 sun.misc.Launcher$AppClassLoader,另外我們知道ClassLoader中有個getSystemClassLoader
方法,此方法返回的正是AppclassLoader.AppClassLoader主要負責加載classpath所指定的位置的類或者是jar文檔,它也是Java程序默認的類加載器。
綜上所述,它們之間的關系可以通過下圖形象的描述:
為什么要有三個類加載器,一方面是分工,各自負責各自的區塊,另一方面為了實現委托模型。
類加載器之間是如何協調工作的
前面說了,java中有三個類加載器,問題就來了,碰到一個類需要加載時,它們之間是如何協調工作的,即java是如何區分一個類該由哪個類加載器來完成呢。 在這里java采用了委托模型機制,這個機制簡單來講,就是“類裝載器有載入類的需求時,會先請示其Parent使用其搜索路徑幫忙載入,如果Parent 找不到,那么才由自己依照自己的搜索路徑搜索類”
下面舉一個例子來說明,為了更好的理解,先弄清楚幾行代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Public
class
Test
{
Public
static
void
main
(
String
[
]
arg
)
{
ClassLoader
c
=
Test
.
class
.
getClassLoader
(
)
;
//獲取Test類的類加載器
System
.
out
.
println
(
c
)
;
ClassLoader
c1
=
c
.
getParent
(
)
;
//獲取c這個類加載器的父類加載器
System
.
out
.
println
(
c1
)
;
ClassLoader
c2
=
c1
.
getParent
(
)
;
//獲取c1這個類加載器的父類加載器
System
.
out
.
println
(
c2
)
;
}
}
|
運行結果:
1
2
3
4
5
|
……
AppClassLoader……
……
ExtClassLoader……
Null
|
可以看出Test是由AppClassLoader加載器加載的,AppClassLoader的Parent
加載器是 ExtClassLoader,但是ExtClassLoader
的Parent
為 null
是怎么回事呵,朋友們留意的話,前面有提到Bootstrap Loader是用C++語言寫的,依java的觀點來看,邏輯上並不存在Bootstrap Loader的類實體,所以在java
程序代碼里試圖打印出其內容時,我們就會看到輸出為null
。
類裝載器ClassLoader(一個抽象類)描述一下JVM加載class文件的原理機制
類裝載器就是尋找類或接口字節碼文件進行解析並構造JVM內部對象表示的組件,在java中類裝載器把一個類裝入JVM,經過以下步驟:
1、裝載:查找和導入Class文件 2、鏈接:其中解析步驟是可以選擇的 (a)檢查:檢查載入的class文件數據的正確性 (b)准備:給類的靜態變量分配存儲空間 (c)解析:將符號引用轉成直接引用 3、初始化:對靜態變量,靜態代碼塊執行初始化工作
類裝載工作由ClassLoder
和其子類負責。JVM在運行時會產生三個ClassLoader:根裝載器,ExtClassLoader
(擴展類裝載器)和AppClassLoader
,其中根裝載器不是ClassLoader的子類,由C++編寫,因此在java中看不到他,負責裝載JRE的核心類庫,如JRE目錄下的rt.jar,charsets.jar等。ExtClassLoader
是ClassLoder
的子類,負責裝載JRE擴展目錄ext下的jar類包;AppClassLoader
負責裝載classpath路徑下的類包,這三個類裝載器存在父子層級關系****,即根裝載器是ExtClassLoader的父裝載器,ExtClassLoader是AppClassLoader的父裝載器。默認情況下使用AppClassLoader裝載應用程序的類
Java裝載類使用“全盤負責委托機制”。“全盤負責”是指當一個ClassLoder
裝載一個類時,除非顯示的使用另外一個ClassLoder
,該類所依賴及引用的類也由這個ClassLoder
載入;“委托機制”是指先委托父類裝載器尋找目標類,只有在找不到的情況下才從自己的類路徑中查找並裝載目標類。這一點是從安全方面考慮的,試想如果一個人寫了一個惡意的基礎類(如java.lang.String
)並加載到JVM
將會引起嚴重的后果,但有了全盤負責制,java.lang.String
永遠是由根裝載器來裝載,避免以上情況發生 除了JVM默認的三個ClassLoder
以外,第三方可以編寫自己的類裝載器,以實現一些特殊的需求。類文件被裝載解析后,在JVM
中都有一個對應的java.lang.Class
對象,提供了類結構信息的描述。數組,枚舉及基本數據類型,甚至void
都擁有對應的Class
對象。Class
類沒有public
的構造方法,Class
對象是在裝載類時由JVM
通過調用類裝載器中的defineClass()
方法自動構造的。
線程上下文類加載器
線程上下文類加載器(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 的實現中都會用到。
類加載器與Web容器
對於運行在 Java EE容器中的 Web 應用來說,類加載器的實現方式與一般的 Java 應用有所不同。不同的 Web 容器的實現方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規范中的推薦做法,其目的是使得 Web 應用自己的類的優先級高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內的。這也是為了保證 Java 核心庫的類型安全。
絕大多數情況下,Web 應用的開發人員不需要考慮與類加載器相關的細節。下面給出幾條簡單的原則:
(1)每個 Web 應用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
(2)多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面。
(3)當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。
類加載器與OSGi
OSGi是 Java 上的動態模塊系統。它為開發人員提供了面向服務和基於組件的運行環境,並提供標准的方式用來管理軟件的生命周期。OSGi 已經被實現和部署在很多產品上,在開源社區也得到了廣泛的支持。Eclipse就是基於OSGi 技術來構建的。
OSGi 中的每個模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導入(import)的其它模塊的 Java 包和類(通過 Import-Package),也可以聲明導出(export)自己的包和類,供其它模塊使用(通過 Export-Package)。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實現的。OSGi 中的每個模塊都有對應的一個類加載器。它負責加載模塊自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(通常是啟動類加載器)來完成。當它需要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設置系統屬性 org.osgi.framework.bootdelegation的值即可。
假設有兩個模塊 bundleA 和 bundleB,它們都有自己對應的類加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類 com.bundleA.Sample,並且該類被聲明為導出的,也就是說可以被其它模塊所使用的。bundleB 聲明了導入 bundleA 提供的類 com.bundleA.Sample,並包含一個類 com.bundleB.NewSample繼承自 com.bundleA.Sample。在 bundleB 啟動的時候,其類加載器 classLoaderB 需要加載類 com.bundleB.NewSample,進而需要加載類 com.bundleA.Sample。由於 bundleB 聲明了類 com.bundleA.Sample是導入的,classLoaderB 把加載類 com.bundleA.Sample的工作代理給導出該類的 bundleA 的類加載器 classLoaderA。classLoaderA 在其模塊內部查找類 com.bundleA.Sample並定義它,所得到的類 com.bundleA.Sample實例就可以被所有聲明導入了此類的模塊使用。對於以 java開頭的類,都是由父類加載器來加載的。如果聲明了系統屬性 org.osgi.framework.bootdelegation=com.example.core.*,那么對於包 com.example.core中的類,都是由父類加載器來完成的。
OSGi 模塊的這種類加載器結構,使得一個類的不同版本可以共存在 Java 虛擬機中,帶來了很大的靈活性。不過它的這種不同,也會給開發人員帶來一些麻煩,尤其當模塊需要使用第三方提供的庫的時候。下面提供幾條比較好的建議:
(1)如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中,在 Bundle-ClassPath中指明即可。
(2)如果一個類庫被多個模塊共用,可以為這個類庫單獨的創建一個模塊,把其它模塊需要用到的 Java 包聲明為導出的。其它模塊聲明導入這些類。
(3)如果類庫提供了 SPI 接口,並且利用線程上下文類加載器來加載 SPI 實現的 Java 類,有可能會找不到 Java 類。如果出現了 NoClassDefFoundError異常,首先檢查當前線程的上下文類加載器是否正確。通過 Thread.currentThread().getContextClassLoader()就可以得到該類加載器。該類加載器應該是該模塊對應的類加載器。如果不是的話,可以首先通過 class.getClassLoader()來得到模塊對應的類加載器,再通過 Thread.currentThread().setContextClassLoader()來設置當前線程的上下文類加載器。