Java類加載

Java類加載

虛擬機把描述類的數據從Class文件加載到內存中,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是Java虛擬機的類加載機制。


先從一個HelloWorld說起,對於一個HelloWorld.java文件,我們在DOS命令行下面使用

javac HelloWorld.java

編譯源程序,生成一個HelloWorld.class的字節碼文件,然後我們使用

java HelloWorld

就可以執行該程序。可是從我們硬盤上的.class文件是如何變成內存中的執行指令的呢?

平臺無關係

Java語言是平臺無關性的語言,可以運行在多個操作系統和硬件架構之上,我們知道Java的宣傳語:“一次編譯,到處執行”(Write Once, Run Anywhere),這是由於在應用程序和操作系統中間有一層是Java虛擬機(也就是你安裝的JRE),虛擬機運行的是字節碼Class文件,你可以把Windows下編譯好的Class文件放到Linux下JVM中運行,而不需要在Linux下重新編譯一次源文件

語言無關性

不僅Java是平臺無關性的語言,其實Java虛擬機(姑且這麼叫吧)也是語言無關性,Java虛擬機運行的是字節碼(ByteCode),而不管這種Class文件是來自何種語言。語言無關性,這是JVM設計之初的願望,現在JVM還可以運行JRuby程序(*.rb)經過編譯器(jruby編譯器)編譯後的字節碼(*.class),還可以運行Groovy經過其編譯器編譯後的Class文件。


先簡單說一下Class文件,Class文件是一組以8字節爲基礎單位的二進制流,各個數據項目按照順序緊湊的排列在Class文件中,中間沒有任何的分隔符,這使得Class文件的內容幾乎都是程序運行時必要的數據,當遇到8字節以上的空間數據項的時候,按照高位在前低位在後的順序排列。

在由Java源代碼編譯而來的Class文件中,前4個字節被稱爲“魔數”,它的唯一作用是確定這個文件是否能夠被Java虛擬機所接受,防止惡意文件的攻擊,一定程度上保護了虛擬機。

緊接着“魔數”的是4個字節的版本號(次版本號+主版本號,次版本號2個字節在前,主版本號在後),主版本號從45.0(十進制)開始(JDK1.1——45.0,JDK1.2——46.0,.......,JDK1.7——51.0,JDK1.8——52.0)數據都是以16進制的數表示,虛擬機可以向下兼容,但不能向上兼容版本號比它大的字節碼文件,否則會拒絕運行。

緊接着版本號的是常量池,常量池可以理解爲Class文件的資源倉庫,佔用空間較大,主要存儲字面量(比如,文本字符串、聲明爲final的常量)和符號引用(類和接口的全限定名、字段或方法的名稱和描述符)。Class文件中還存放一些其他有用的信息,比如訪問標誌等,總的來說,Class文件是描述Java類的數據,有着相當嚴格的格式定義。

任何一個Class文件都對應着唯一一個類或者接口的定義信息,但類或者接口並不一定非要定義在Class文件中,也可以通過類加載器直接生成。


再回答那個問題,從我們硬盤上的.class文件是如何變成內存中的執行指令的呢?或者,從Class文件是怎麼被加載到Java虛擬機中運行的呢?


Java類的生命週期



詳細過程分析:一個.class文件被加載到內存會進行如下的步驟:加載、連接、初始化

加載:就是類加載器ClassLoader查找並加載類的二進制數據到內存的方法區,並在虛擬機的堆中創建一個Class對象的實例,加載的方式可以是本地直接加載也可以是網絡下載.class文件或者直接從jar、zip等歸檔文件中加載等等;
連接:具體可以分爲驗證、準備、解析三個步驟

a驗證:確保被加載類的正確性,“魔數”以及版本號的驗證,包括類的結構檢查、語義檢查和字節碼檢查等等;

b準備:爲靜態變量分配內存空間,並將其初始化,這裏的初始化是賦予默認的初始值,如int型則賦予0,boolean型則賦予false;

c解析:將類中的符號引用轉化爲直接引用(加載引用類)

初始化:爲類的靜態變量賦予正確的初始值,也就是賦予用戶對其設置的初始值,和上面的初始化是不同的。


解決3個問題:

何時加載?

怎麼加載?

加載到虛擬機發生了什麼?

答:何時加載?——分爲懶加載與立即加載,懶加載是當被其他類引用時不立即加載,而是等到使用時再去加載;立即加載是被引用時就全部加載。。

怎麼加載?——通過類加載器進行加載。

加載到虛擬機發生了什麼?——具體的過程可參見生命週期圖描述。


類加載器(Class Loader)

類加載器的工作過程簡單的說就是類加載器會將.class文件中的二進制數據讀取到內存中,將其放入到JVM運行時數據區的方法區內,然後在java虛擬機的堆中(虛擬機規範中沒有明確指明,對於HotSpot虛擬機而言,Class對象是在方法區內的)創建一個java.lang.Class對象用來封裝類在方法區內的數據結構。Java類加載的最終結果是生成了堆中的Class對象。


補充一點:對於這個Class對象其實在編譯時就已經存在,無論何時,編譯器在編譯Java源文件的時候都會在編譯後的字節碼中嵌入一個public、static、final類型的字段class,這個字段表示java.lang.Class的一個實例,因爲它是靜態的public的,所以,很多時候我們可以通過類名.class來訪問它。


Java類的加載是由類加載器來完成的。一般來說,類加載器分成兩類:啓動類加載器(bootstrap)和其它類加載器(other)。兩者的區別在於啓動類加載器是由JVM的原生代碼實現的,而其它類加載器都繼承自Java中的java.lang.ClassLoader類。在其它類加載器的部分,一般JVM都會提供一些基本實現。應用程序的開發人員也可以根據需要編寫自己的類加載器。JVM中最常使用的是系統類加載器(system),它用來啓動Java應用程序的加載。通過java.lang.ClassLoader的getSystemClassLoader()方法可以獲取到該類加載器對象。

類加載器的模型


啓動類加載器(BootStrap ClassLoader)也叫根類加載器,是由C++編寫,它是唯一一個沒有父加載器的加載器,用於加載JAVA_HOME\lib目錄下,或者被-Xbootclasspath參數指定的目錄下的,並且被虛擬機識別的類(未被識別的類是無法加載到,比如,隨便放一個原來該目錄沒有的jar進去,是不會被夾在的)。

擴展類加載器(Extension ClassLoader),由Java編寫,繼承與抽象類java.lang.ClassLoader,用於加載JAVA_HOME\lib\ext目錄下,或者被java.ext.dirs指定路徑的所有類,開發者可以直接使用擴展類加載器。

應用類加載器(Application ClassLoader)也叫系統類加載器,由JAVA編寫,繼承與抽象類java.lang.ClassLoader,用於加載CLASSPATH所制定路徑下的所有類,使用getSystemClassLoader()方法能夠獲取此加載器,可以直接使用應用類加載器。


各個類加載器之間是通過雙親委派模型進行協調工作的,雙親委派模型是JDK1.2之後引入的。

雙親委派模型:當一個類加載器收到類加載的請求的時候,不會立即去加載這個類,而是委託請求它的父加載器去加載這個類,如果父加載器能夠加載就由父加載器加載,只有當父加載器在其目錄下無法加載該類的時候,自己纔會去加載。

特別說明,雙親委派模型並不是強制性的約束模型,而是Java設計者的一個推薦的類加載器實現方式,現在Java世界大多遵循這種方式。但是,Servlet規範以及Apache Tomcat就拋棄了雙親委派模型,Tomcat每個應用擁有一個自己的加載器,需要加載某個類的時候,會嘗試先自己加載某類,如果無法加載,再請求父加載器。


這樣做的好處:我們知道對於同樣一個Class,由不同的類加載器加載進虛擬機,形成的對象是不相等的。雙親委派模型可以解決這種機制帶來的“誤傷”,例如:java.lang.Object,它放在rt.jar中,無論那個類加載器加載Object類,都會委託頂端的啓動類加載器(BootStrap ClassLoader)去加載它,這就保證了所有被加載進虛擬機的Object類是同一個。


雙親委派模型的邏輯實現封裝在ClassLoader中loadClass()方法:


protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {          //該類還沒有被加載
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {         //存在父加載器
                        c = parent.loadClass(name, false);
                    } else {<span style="white-space:pre">			</span> //不存在父加載器,也就是BootStrap ClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {   //父類加載失敗或者BootStrap ClassLoader加載失敗
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);         //自己加載

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }


從源碼可以看到類加載器加載一個類的過程:如果一個類沒有被加載,如果存在父加載器,則委託父加載器,父加載器無法完成,則嘗試自己加載。loadClass()方法封裝了雙親委派模型,用戶自己的加載邏輯可以在findClass()方法中實現,然後有上面loadClass的源碼可知,loadClass()會在父加載器無法加載的時候,調用findClass()方法。抽象類ClassLoader中的findClass()方法如下:


protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }


沒有提供任何實現,如果在自定義類加載器中,沒有重寫findClass()方法,當loadClass調用時,會拋出一個ClassNotFoundException。


ClassLoader 中與加載類相關的方法
方法 說明
getParent() 返回該類加載器的父類加載器。
loadClass(String name) 加載名稱爲 name的類,返回的結果是 java.lang.Class類的實例。
findClass(String name) 查找名稱爲 name的類,返回的結果是 java.lang.Class類的實例。
findLoadedClass(String name) 查找名稱爲 name的已經被加載過的類,返回的結果是 java.lang.Class類的實例。
defineClass(String name, byte[] b, int off, int len) 把字節數組 b中的內容轉換成 Java 類,返回的結果是 java.lang.Class類的實例。這個方法被聲明爲 final的。
resolveClass(Class<?> c) 鏈接指定的 Java 類。
下面介紹一種加載類的方法:Class.forName
Class.forName是一個靜態方法,同樣可以用來加載類。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一種形式的參數 name表示的是類的全名;initialize表示是否初始化類;loader表示加載時使用的類加載器。第二種形式則相當於設置了參數 initialize的值爲 true,loader的值爲當前類的類加載器。Class.forName的一個很常見的用法是在加載數據庫驅動的時候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來加載 Apache Derby 數據庫的驅動。


自定義類加載器
在介紹完類加載器相關的基本概念之後,下面介紹如何開發自己的類加載器。

雖然在絕大多數情況下,系統默認提供的類加載器實現已經可以滿足需求。但是在某些情況下,您還是需要爲應用開發出自己的類加載器。比如您的應用通過網絡來傳輸 Java 類的字節代碼,爲了保證安全性,這些字節代碼經過了加密處理。這個時候您就需要自己的類加載器來從某個網絡地址上讀取加密後的字節代碼,接着進行解密和驗證,最後定義出要在 Java 虛擬機中運行的類來。
package classLoader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader {

	private String rootDir;
	
	public MyClassLoader(String path) {
		this.rootDir = path;
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		 byte[] classData = getClassData(name); 
	        if (classData == null) { 
	            throw new ClassNotFoundException(); 
	        } else { 
	            return defineClass(name, classData, 0, classData.length); 
	        }
	}
	
	private byte[] getClassData(String className) { 
        String path = classNameToPath(className); 
        try { 
            InputStream ins = new FileInputStream(path); 
            ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
            int bufferSize = 4096; 
            byte[] buffer = new byte[bufferSize]; 
            int bytesNumRead = 0; 
            while ((bytesNumRead = ins.read(buffer)) != -1) { 
                baos.write(buffer, 0, bytesNumRead); 
            } 
            return baos.toByteArray(); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
        return null; 
    } 

    private String classNameToPath(String className) {
    	
        return rootDir + File.separatorChar 
                + className.replace('.', File.separatorChar) + ".class"; 
    }
	
}
測試該加載器是否能夠加載
package classLoader;

import java.lang.reflect.Constructor;

public class Test {

	public static void main(String[] args){
		String path = "D:\\Workspaces\\eclipse-kepler-java\\design_pattern\\bin"; //硬盤上的絕對路徑
		
		MyClassLoader mc = new MyClassLoader(path);
		Class<?> clazz = null;
		try {
			clazz = mc.loadClass("classLoader.Cat");  //加載Cat類
			Constructor<?> cons = clazz.getConstructor(String.class,int.class);  //獲取構造器
			Cat c3 = (Cat)cons.newInstance("pp",20); //生成一個實例
			c3.say();  //調用實例方法
		} catch (Exception e) {
			e.printStackTrace();
		} 
	
	}
}
Cat類的內容如下

package classLoader;

public class Cat {

	private String name;
	private int age;
	
	public Cat(String name, int age) {
		this.name = name;
		this.age = age;
	}
	
	public void say() {
		System.out.println("My name is "+name+", My age is "+age);
	}
	
}

一般來說,自己開發的類加載器只需要繼承類 java.lang.ClassLoader,覆寫 findClass(String name)方法即可。java.lang.ClassLoader類的方法 loadClass()封裝了前面提到的代理模式的實現。該方法會首先調用 findLoadedClass()方法來檢查該類是否已經被加載過;如果沒有加載過的話,會調用父類加載器的 loadClass()方法來嘗試加載該類;如果父類加載器無法加載該類的話,就調用 findClass()方法來查找該類。因此,爲了保證類加載器都正確實現代理模式,在開發自己的類加載器時,最好不要覆寫 loadClass()方法,而是覆寫 findClass()方法。
類 MyClassLoader的 findClass()方法首先根據類的全名在硬盤上查找類的字節代碼文件(.class 文件),然後讀取該文件內容,最後通過 defineClass()方法來把這些字節代碼轉換成 java.lang.Class類的實例。


詳情可參考IBM DeveloperWork的 深入探討 Java 類加載器



發佈了65 篇原創文章 · 獲贊 34 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章