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;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
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(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 數據庫的驅動。
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 類加載器