類加載的時機
類從被加載到虛擬機內(nèi)存中開始戒洼,到卸載出內(nèi)存為止掏愁,它的整個生命周期包括:加載(Loading)、驗證(Verification)场航、準備(Preparation)、解析(Resolution)廉羔、初始化(Initialization)溉痢、使用(Using)和卸載(Unloading)7個階段。其中 驗證憋他、準備孩饼、解析3個部分統(tǒng)稱為鏈接(Link),這7個階段的發(fā)生順序如下圖所示:
類加載的過程
接下來我們詳細說一下Java虛擬機中類加載的全過程竹挡,也就是 加載镀娶、驗證、準備此迅、解析汽畴、初始化這5個階段所執(zhí)行的具體動作。
1、加載
"加載"是"類加載"(Class Loading)過程的一個階段。在加載階段宜鸯,虛擬機需要完成以下3件事:
- 通過一個的全限定名來獲取定義此類的二進制字節(jié)流岖沛。
- 將這個字節(jié)流所代表的靜態(tài)存儲結構轉換為方法區(qū)的運行時數(shù)據(jù)結構。
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象廓握,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口搅窿。
2、驗證
驗證是 鏈接階段的第一步隙券,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求男应,并且不會危害虛擬機自身的安全。
驗證階段大致上會完成下面4個階段的校驗動作:文件格式驗證娱仔、元數(shù)據(jù)驗證沐飘、字節(jié)碼驗證、符號引用驗證。
3耐朴、準備
準備階段是正式為類變量分配內(nèi)存并設置變量初始值的階段借卧,這些變量所使用的內(nèi)存都將在方法區(qū)中的進行分配。
這個階段中有兩個容易產(chǎn)生混淆的地方:
首先筛峭,這時候進行內(nèi)存分配的僅包括類變量(被static修飾的變量)铐刘,而不包括示例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中影晓。
其次镰吵,這里所說的初始值 "通常情況"下是數(shù)據(jù)類型的默認值。
4挂签、解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程疤祭。
5、初始化
類初始化是類加載過程中的最后一步竹握,前面的類加載過程中画株,除了在加載階段用戶應用程序可以通過 自定義類加載器參與之外,其余動作完全由虛擬機主導和控制啦辐。到了初始化階段谓传,才真正開始執(zhí)行類中定義的Java程序代碼(或者說字節(jié)碼)。
什么是類加載器
虛擬機設計團隊吧類加載階段中的 "通過一個類的全限定名來獲取描述此類的二進制字節(jié)流"這個動作放到虛擬機外部去實現(xiàn)芹关,以便讓應用程序自己覺得如何去獲取所需要的類续挟。實現(xiàn)這個動作的代碼模塊稱為 "類加載器"。
顧名思義侥衬,類加載器(ClassLoader )用來加載 Java 類到 Java 虛擬機中诗祸。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉換成 Java 字節(jié)代碼(.class 文件)轴总。類加載器負責讀取 Java 字節(jié)代碼直颅,并轉換成 java.lang.Class類的一個實例。每個這樣的實例用來表示一個 Java 類怀樟。
ClassLoader 類
A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.
ClassLoader 中與加載類相關的方法
方法 | 說明 |
---|---|
getParent() | 返回該類加載器的父類加載器功偿。 |
loadClass(String name) | 加載名稱為 name的類,返回的結果是 java.lang.Class類的實例往堡。 |
findClass(String name) | 查找名稱為 name的類械荷,返回的結果是 java.lang.Class類的實例。 |
findLoadedClass(String name) | 查找名稱為 name的已經(jīng)被加載過的類虑灰,返回的結果是 java.lang.Class類的實例吨瞎。 |
defineClass(String name, byte[] b, int off, int len) | 把字節(jié)數(shù)組 b中的內(nèi)容轉換成 Java 類,返回的結果是 java.lang.Class類的實例穆咐。這個方法被聲明為 final的颤诀。 |
resolveClass(Class<?> c) | 鏈接指定的 Java 類字旭。 |
loadClass(String name)方法源碼如下:
public abstract class ClassLoader {
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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 {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 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方法主要流程:
- 調(diào)用findLoadedClass(String) 方法檢查這個類是否被加載過,如果已加載則跳到第4步着绊,
- 如果父加載器存在則調(diào)用父加載器調(diào)用 loadClass(String) 方法谐算,否則,交給BootStrap ClassLoader 來加載此類归露,
- 如果執(zhí)行完上述2步驟 還沒有找到對應的類洲脂,則調(diào)用當前類加載器的findClass(String) 。
- 按照以上的步驟成功的找到對應的類剧包,并且該方法接收的 resolve 參數(shù)的值為 true,那么就調(diào)用resolveClass(Class) 方法來處理類恐锦。
類加載器在成功加載某個類之后,會把得到的 java.lang.Class類的實例緩存起來疆液。下次再請求加載該類的時候一铅,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載堕油。也就是說潘飘,對于一個類加載器實例來說,相同全名的類只加載一次掉缺,即 loadClass方法不會被重復調(diào)用卜录。
java.lang.ClassLoader類的方法 loadClass()封裝了類加載器的雙親委托模型的實現(xiàn)。因此眶明,為了保證類加載器都正確實現(xiàn) 類加載器的雙親委托模型艰毒,在開發(fā)自己的類加載器時,最好不要覆寫 loadClass()方法搜囱,而是覆寫 findClass()方法丑瞧。
ClassLoader 體系
Java 中的類加載器大致可以分成兩類,一類是系統(tǒng)提供的蜀肘,另外一類則是由 Java 應用開發(fā)人員編寫的绊汹。系統(tǒng)提供的類加載器主要有下面三個:
- BootStrap ClassLoader:稱為啟動類加載器,是Java類加載層次中最頂層的類加載器扮宠,負責加載JDK中的核心類庫灸促,如:rt.jar、resources.jar涵卵、charsets.jar等,可通過如下程序獲得該類加載器從哪些地方加載了相關的jar或class文件:
public class BootStrapTest
{
public static void main(String[] args)
{
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
}
}
Extension ClassLoader:稱為擴展類加載器荒叼,負責加載Java的擴展類庫轿偎,Java 虛擬機的實現(xiàn)會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java 類被廓。默認加載JAVA_HOME/jre/lib/ext/目下的所有jar坏晦。
App ClassLoader:稱為系統(tǒng)類加載器,負責加載應用程序classpath目錄下的所有jar和class文件。一般來說昆婿,Java 應用的類都是由它來完成加載的球碉。可以通過 ClassLoader.getSystemClassLoader()來獲取它仓蛆。
除了系統(tǒng)提供的類加載器以外睁冬,開發(fā)人員可以通過繼承java.lang.ClassLoader類的方式實現(xiàn)自己的類加載器,以滿足一些特殊的需求看疙。
Bootstrap ClassLoader
|
Extension ClassLoader
|
App ClassLoader
/ \
自定義ClassLoader1 自定義ClassLoader2
除了引導類加載器之外豆拨,所有的類加載器都有一個父類加載器。 給出的 getParent()方法可以得到能庆。
對于系統(tǒng)提供的類加載器來說施禾,系統(tǒng)類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器搁胆;對于開發(fā)人員編寫的類加載器來說弥搞,其父類加載器是加載此類加載器 Java 類的類加載器。
類加載器的代理模式
類加載器在嘗試自己去查找某個類的字節(jié)代碼并定義它時渠旁,會先代理給其父類加載器攀例,由父類加載器先去嘗試加載這個類,依次類推一死。
當一個ClassLoader實例需要加載某個類時肛度,它會試圖親自搜索某個類之前,先把這個任務委托給它的父類加載器投慈,這個過程是由上至下依次檢查的承耿,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,如果沒加載到伪煤,則把任務轉交給Extension ClassLoader試圖加載加袋,如果也沒加載到,則轉交給App ClassLoader進行加載抱既,如果它也沒有加載得到的話职烧,則返回給委托的發(fā)起者,由它到指定的文件系統(tǒng)或網(wǎng)絡等URL中加載該類防泵。如果它們都沒有加載到這個類時蚀之,則拋出ClassNotFoundException異常。
這種機制又稱作做 類加載器的雙親委托模型捷泞。
Java 虛擬機是如何判定兩個 Class 是相同的
- JVM在判定兩個class是否相同時足删,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的锁右。只有兩者同時滿足的情況下失受,JVM才認為這兩個class是相同的讶泰。*
即便是同樣的字節(jié)代碼,被不同的類加載器加載之后所得到的類拂到,也是不同的痪署。比如一個 Java 類 com.example.Sample,編譯之后生成了字節(jié)代碼文件 Sample.class兄旬。兩個不同的類加載器 ClassLoaderA和 ClassLoaderB
分別讀取了這個 Sample.class文件狼犯,并定義出兩個 java.lang.Class類的實例來表示這個類。這兩個實例是不相同的辖试。對于 Java 虛擬機來說辜王,它們是不同的類。試圖對這兩個類的對象進行相互賦值罐孝,會拋出運行時異常java.lang.ClassCastException
呐馆。
下面通過示例來具體說明。
Java 類 com.example.Sample:
package com.example;
public class Sample {
private Sample instance;
public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
ClassIdentityDemo
public class ClassIdentityDemo {
public static void main(String[] args) {
new ClassIdentityDemo().testClassIdentity();
}
public void testClassIdentity() {
String classDataRootPath = "F:\\github\\daily-codelab\\classloader-sample\\target\\classes";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.bytebeats.classloader.sample.ch3.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
FileSystemClassLoader
import java.io.*;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2017-03-12 17:24
*/
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream in = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = in.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";
}
}
為什么要使用代理模式
了解了這一點之后莲兢,就可以理解代理模式的設計動機了汹来。代理模式是為了保證 Java 核心庫的類型安全。所有 Java 應用都至少需要引用 java.lang.Object類改艇,也就是說在運行的時候收班,java.lang.Object這個類需要被加載到 Java 虛擬機中。如果這個加載過程由 Java 應用自己的類加載器來完成的話谒兄,很可能就存在多個版本的 java.lang.Object類摔桦,而且這些類之間是不兼容的。通過代理模式承疲,對于 Java 核心庫的類的加載工作由引導類加載器來統(tǒng)一完成邻耕,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的燕鸽。
不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間兄世。相同名稱的類可以并存在 Java 虛擬機中,只需要用不同的類加載器來加載它們即可啊研。不同類加載器加載的類之間是不兼容的御滩,這就相當于在 Java 虛擬機內(nèi)部創(chuàng)建了一個個相互隔離的 Java 類空間。這種技術在許多框架中都被用到党远,例如OSGI削解、Web 容器(Tomcat)。
類加載器與 Web 容器
對于運行在 Java EE?容器中的 Web 應用來說沟娱,類加載器的實現(xiàn)方式與一般的 Java 應用有所不同钠绍。不同的 Web 容器的實現(xiàn)方式也會有所不同。以 Apache Tomcat 來說花沉,每個 Web 應用都有一個對應的類加載器實例柳爽。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類碱屁,如果找不到再代理給父類加載器磷脯。這與一般類加載器的順序是相反的。這是 Java Servlet 規(guī)范中的推薦做法娩脾,其目的是使得 Web 應用自己的類的優(yōu)先級高于 Web 容器提供的類赵誓。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內(nèi)的。這也是為了保證 Java 核心庫的類型安全柿赊。
參考資料
源代碼
所有源碼均已上傳給Github