衆所周知,JAVA 包含四種 類加載器,分別是
Bootstrap ClassLoader 根類加載器 :
加載核心類庫 lib 目錄下的jar和class文件
可以通過如下語句打印根類加載的文件
System.out.println(System.getProperty("sun.boot.class.path"));
Extension ClassLoader 擴展類加載器:
加載lib/ext下的jar和class文件
可以通過如下語句打印擴展類加載的文件
System.out.println(System.getProperty("java.ext.dirs"));
AppClassLoader 應用類加載器;
加載應用程序中的classpath指定的jar和class文件
可以通過如下語句打印應用類加載的文件
System.out.println(System.getProperty("java.class.path"));
URLClassLoader 用來加載網絡上遠程的類
1、先簡單說一下ClassLoader的原理
當執行 java ***.class 的時候, java.exe 會幫助我們找到 JRE ,接着找到位於 JRE 內部的 jvm.dll ,這纔是真正的 Java 虛擬機器 , 最後加載動態庫,激活 Java 虛擬機器。虛擬機器激活以後,會先做一些初始化的動作,比如說讀取系統參數等。一旦初始化動作完成之後,就會產生第一個類加載器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰寫而成,這個 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化動作之外,最重要的就是加載 Launcher.java 之中的 ExtClassLoader ,並設定其 Parent 爲 null ,代表其父加載器爲 BootstrapLoader 。然後 Bootstrap Loader 再要求加載 Launcher.java 之中的 AppClassLoader ,並設定其 Parent 爲之前產生的 ExtClassLoader 實體。這兩個加載器都是以靜態類的形式存在的。這裏要請大家注意的是, Launcher$ExtClassLoader.class 與 Launcher$AppClassLoader.class 都是由 Bootstrap Loader 所加載,所以 Parent 和由哪個類加載器加載沒有關係。
(引用 http://blog.csdn.net/feier7501/article/details/19133009)
首先我貼一下代碼:
public class TestClassLoader {
public static void main(String[] args) throws Exception {
System.out.println("HelloWorld!");
ClassLoader cl=TestClassLoader.class.getClassLoader();
System.out.println(cl);
}
}
運行結果如下:
HelloWorld!
sun.misc.Launcher$AppClassLoader@593887c2
2、類加載過程
我們都知道,java的類加載器設計使用的是雙親委託模型,當前的類加載器(應用類加載器)收到一條加載指令,首先由頂層的類加載器即根類加載器進行加載,如果根類沒有找到這個類,則交給擴展類加載器進行加載,如果還沒有加載到,則交給應用類加載器進行加載,如果都加載不到,則拋出ClassNotFoundException 的異常。當加載類的時候,虛擬機需要完成三件事:
(1)通過類的全限定名來獲取定義此類的二進制字節流
(2)將字節流所代表的的靜態存儲結構轉化爲方法區的運行時數據結構
(3)在內存中生成一個代表這個類的java.lang.Class的對象,作爲方法區這個類的各種數據的訪問入口/
注:獲取二進制字節流的方法除了常規的通過java文件編譯後的class 文件,也可以從網絡中獲取,zip讀取,運行時計算生成,其他文件生成或者從數據庫中讀取。
完成了加載工作,下一步就是驗證準備。驗證是爲了保證數據的合法性。
準備階段則是類變量分配內存並設置類變量初始值的階段。類變量是指類中定義的static修飾的變量,實例變量將會在對象實例化時隨對象一起被分配在Java堆中。除此之外,如果變量被final修飾,則會爲變量生成ConstatnValue屬性,在準備階段,虛擬機就會給變量賦值,準備階段注意的是爲變量賦初始值,比如int 類型的數據初始值是0,但是 int a=3 爲a賦值3的操作並沒有發生在準備階段。
準備工作完成,則開始進行解析。解析工作主要是講常量池內的符號引用替換爲直接引用。 這個階段目前還沒有怎麼研究透,筆者不多說。
解析完畢,則開始進行初始化,前面我們提到的static修飾的類變量,在初始化階段完成初始化。初始化階段是執行類構造器<clinit>()方法的過程。由此我們引出幾個要點,
a、初始化時機
虛擬機規範了有且只有5中情況需要立即對類進行初始化
(1)遇到new、getstatic、putstatic、invokestatic四條指令時,在我們寫程序的時候分別對應如下幾種情況,new 對象,讀取或設置一個類的靜態字段(被final修飾的除外,被final修飾,編譯的時候就把被修飾的數據放入常量池),調用定靜態方法
(2)使用反射對類調用,類還沒有進行過初始化,則先觸發初始化
(3)初始化類的時候,父類還沒有進行過初始化,則先進行父類的初始化
(4)虛擬機啓動,包含main方法的主類
(5)不常見,jdk1.7動態語言支持,如果一個java.lang.invoke.MethodHandle的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行初始化,則觸發初始化
以上行爲都稱爲對一個類的主動引用,、除此之外,還有一個被動引用的概念。
(1)對於靜態字段,只有直接定義這個字段的類纔會被初始化。父類定義靜態字段,子類繼承並使用這個字段只會觸發父類的初始化,而不會觸發子類 的初始化。
(2)創建數組, SuperClass[] sca=new SuperClass[10]; 這種情況下,不會觸發SuperClass類的初始化動作。
(3)訪問常量
b、clinit 方法介紹
(1) 該方法是編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊彙總的語句合併產生的,收集順序與源文件中出現的順序一樣,也就是我們定義static變量的先後順序。靜態語句塊可以給靜態變量賦值,但是如果該靜態變量的訪問發生在定義之前,則編譯不過。
代碼如下:
public class Test{
static{
i=0; // 編譯通過
System.out.println(i);//編譯提示非法向前引用
}
static int i=1;
}
(2)該方法與類的構造函數不同。,不需顯示的調用父類構造器,虛擬機會保證在子類的clinit()方法執行之前,父類的clinit 方法已經執行完畢。那麼由此可以判斷,第一個被執行clinit 方法的類則是Object。
(3)clinit不是必需的,如果類彙總沒有靜態語句塊,和靜態變量賦值操作,則編譯器不申城clinit 方法
(4)接口中如果有靜態變量賦值,也會生成clinit方法,但是如果有父接口,只有在使用父接口定義的靜態變量時,纔會初始化父接口,另外實現類在初始化時也不會執行接口的clinit方法。
(5)虛擬機會保證一個類的clinit 方法在多線程環境中被正確的加鎖、同步,如果多線程同時去初始化一個類,那麼只有一個線程會去執行這個類的clinit方法,其他線程需要阻塞等待,
c、加載順序
父類靜態代碼(代碼塊和靜態變量出現的先後順序)--》子類的靜態代碼塊--》父類非靜態--》父類構造--》子類非靜態--》子類構造
3、自定義類加載器實現
class MyClassLoader extends ClassLoader{
//類加載器名稱
private String name;
//加載類的路徑
private String path = "D:/";
private final String fileType = ".class";
public MyClassLoader(String name){
//讓系統類加載器成爲該 類加載器的父加載器
super();
this.name = name;
}
public MyClassLoader(ClassLoader parent, String name){
//顯示指定該類加載器的父加載器
super(parent);
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return this.name;
}
/**
* 獲取.class文件的字節數組
* @param name
* @return
*/
private byte[] loaderClassData(String name){
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
this.name = this.name.replace(".", "/");
try {
is = new FileInputStream(new File(path + name + fileType));
int c = 0;
while(-1 != (c = is.read())){
baos.write(c);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
is.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
/**
* 獲取Class對象
*/
@Override
public Class<?> findClass(String name){
byte[] data = loaderClassData(name);
return this.defineClass(name, data, 0, data.length);
}
}
4、自定義類加載器使用場景
一般來講,由JAVA提供的類加載器已經可以滿足大部分功能需要,但是爲什麼還需要自定義類加載器呢?綜合考慮,可能有以下幾個方面的原因:
(1)文件加密,java 編譯後的文件可以通過反編譯獲取到源碼,處於安全的考慮,可能需要對class文件加密,這樣的話當得到了加密後的class文件,通過算法解密之後,再通過類加載器加載。
(2)非標準的類數據獲取來源,前文提到,類加載器獲取定義類的二進制流的時候,除了常規的class文件,也可以從網絡中、zip、數據庫中來加載。
(3)動態創建
5、Tomcat6自定義類加載器實現
public synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class clazz = null;
// Log access to stopped classloader
if (!started) {
try {
throw new IllegalStateException();
} catch (IllegalStateException e) {
log.info(sm.getString("webappClassLoader.stopped", name), e);
}
}
// 檢查當前類加載器有沒有加載過該類
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// 檢查父類加載器有沒有加載過該類
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
//試圖通過系統類加載器來
try {
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
<span style="white-space:pre"> </span>//通過標誌位判斷是否傳給父類進行加載
boolean delegateLoad = delegate || filter(name);
/
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
ClassLoader loader = parent;//首先獲取父類加載器
if (loader == null)<span style="white-space:pre"> </span> //如果父類加載器是空的, 則默認加載器是系統類加載器
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
}
// 自身加載
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
// 父類加載器加載
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
}
throw new ClassNotFoundException(name);
}
基本上tomcat加載的思路是,先判斷類是否被已經被加載,先判斷當前類加載器,再判斷父類加載器,如果沒有,則首先由系統類加載器進行加載,如果還沒有成功,根據delegate標誌位判斷是否由父類加載器加載,如果是,則先由父類加載器進行加載,加載成功,返回,加載失敗則由當前類加載器進行加載,如果還是失敗,則交由給父類進行加載,如果加載不成功,則拋出異常,也就是說,當發生加載行爲的時候,tomcat的webappClassLoader的做法是先由當前類加載器進行加載,然後交給父類,與我們所瞭解的雙親委派模式設計的加載順序不同,
7、補充類加載過程的幾個實驗程序
public class TestClassLoader {
public static void main(String[] args) throws Exception {
System.out.println(Child.p1);
}
}
class Parent {
public static int p1=3;
public static Parent c=new Child();
static {
System.out.println("父類靜態代碼塊");
}
public Parent(){
System.out.println("父類構造函數");
}
}
class Child extends Parent{
public static int c2=4;
public static Parent p=new Parent();
public static final Single s=new Single();
static{
System.out.println("子類靜態代碼塊");
}
public Child(){
System.out.println("子類構造函數");
}
}
運行結果如下:父類構造函數
Single
子類靜態代碼塊
父類構造函數
子類構造函數
父類靜態代碼塊
3
這裏需要注意的情況則是,父類初始化過程中如果遇到對象創建的指令,即父類代碼走到 new Child()的時候,子類中有個 new Parent的指令,但此時父類尚未完全初始化完畢,至於java如何處理這種情況,目前尚未找到相關資料來說明這點,這塊先放着!!!