虛擬機類加載機制
1. 類加載
類加載機制指虛擬機把描述類的數據從Class文件加載到內存中,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型。
在Java代碼中,類型的加載、連接與初始化過程都是在程序運行期間完成的。
在運行期間的好處是更加靈活,增加了更多的可能性。如反射、動態代理等。
1.1 類的生命週期
類的生命週期包括:加載、驗證、準備、解析(Resolution)、初始化、使用和卸載 7 個階段。
1.2 類加載流程
1.3 類的加載、連接與初始化
- 加載:查找並加載類的二進制數據(即
Class
文件) - 連接:
- 驗證:確保被加載的類的正確性
- 準備:爲類的靜態變量分配內存,並將其初始化爲默認值
- 解析:把類中的符號引用轉換爲直接引用
- 初始化:爲類的靜態變量賦予正確的初始值
注意:並不意味着執行類的加載就一定會進行連接、初始化。
1.3.1 加載
類的加載指的是將類的Class
文件中讀入到內存中,將其放在運行時數據區的方法區內,然後在內存中創建一個java.lang.Class
對象。
類加載的時機
Java
虛擬機規範中並沒有進行強制約束,這點可以交給虛擬機的具體實現來自由把握。
1.3.2 連接
1.3.2.1 驗證
對Class
文件的內容進行驗證,需要其符合Java
的語法和要求。
- 文件格式驗證
- 元數據驗證
- 字節碼驗證
- 符號引用驗證
1.3.2.2 準備
準備階段爲類變量在方法區中分配內存並初始化。注意這裏只是對類變量,不包含實例變量。對類變量的初始化是指初始化爲類型默認值。
如果類變量爲編譯期常量,那麼JVM
在準備階段便可以直接賦值。
1.3.2.3 解析
解析階段指虛擬機將常量池內的符號引用替換爲直接引用的過程,這樣我們便可以直接通過對象或類調用其成員。
符號引用以一組符號來描述所引用的目標,可以是任何形式的字面量。
直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。
1.3.3 初始化
類初始化階段是類加載過程的最後一步,初始化階段,才真正開始執行類中定義的Java程序代碼.
public static int a = 1;
在準備階段a
被複製爲0,在初始化階段,a
被賦值爲1,所以初始化階段纔算真正執行按我們意願所進行的初始化操作.
1.3.3.1 類的初始化時機
Java虛擬機對於類初始化的時機進行指定了嚴格的規定。所有的Java虛擬機實現必須在每個類或接口被Java程序“首次主動使用”時才初始化他們。
Java程序對類的使用分爲兩種:
- 主動使用
- 被動使用
1.3.3.1.1 主動使用
- 創建類的實例
- 訪問某個類或接口的靜態變量,或者對該靜態變量賦值(被
final
修飾的編譯器常量除外) - 調用類的靜態方法
- 初始化一個類的子類
- 反射
- 具有
main
方法的類 - JDK1.7開始提供的對動態語言的支持,如果一個
java.lang.invoke.MethodHandle
實例的解析結果REF_getStatic
,REF_putStatic
,REF_invokeStatic
,REF_newInvokeSpecial
四種類型的方法句柄,並且句柄對應的類沒有初始化,則初始化。 - 當一個接口具有默認方法(即被
default
註解修飾),那麼其自其實例類被初始化之前初始化。
1.3.3.1.2 被動使用
除了上述7種情況下,其他使用Java類的方式都被看作是對類的被動使用(可能會加載、連接類也不可能不會),都不會導致類的初始化。
示例一
class MyParent1{
public static String str = "hello world";
static {
System.out.println("MyParent1 static block");
}
}
class MyChild1 extends MyParent1{
public static String str2 = "welcome";
static {
System.out.println("MyChild1 static block");
}
}
public class MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str);
}
}
輸出:
MyParent1 static block
hello world
對於靜態字段來說,只有直接定義了該字段的類纔會被初始化,不管是誰調用,都只會導致該字段所在的類被初始化。 爲什麼MyChild1
沒有被初始化呢?這是因爲對MyChild1
的調用屬於被動調用。
示例二
public class MyTest4 {
public static void main(String[] args) {
MyParent4[] myParent4s = new MyParent4[5];
System.out.println(myParent4s.getClass());
// class [Lcom.wangzhao.jvm.classloader.MyParent4;
MyParent4[][] myParent4s2 = new MyParent4[5][1];
System.out.println(myParent4s2.getClass());
// class [[Lcom.wangzhao.jvm.classloader.MyParent4;
System.out.println(myParent4s.getClass().getSuperclass());
// class java.lang.Object
System.out.println(myParent4s2.getClass().getSuperclass());
// class java.lang.Object
int[] array = new int[1];
System.out.println(array.getClass());
// class [I
System.out.println(array.getClass().getSuperclass());
// class java.lang.Object
}
}
class MyParent4{
static{
System.out.println("MyParent4 static block");
}
}
通過輸出可以看到並沒有輸出 MyParent4 static block
,這是因爲創建MyParent4
類型的數組,並不屬於對MyParent4
的主動使用,在上述主動使用中並不包含這種情況.
對於數組實例來說,其類型是由JVM在運行期動態生成的。
1.3.3.1.3 類的初始化示例
示例一
我們更改被動使用示例一的main
方法,如下所示:
public class MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str2);
}
}
輸出:
MyParent1 static block
MyChild1 static block
welcome
當一個類在初始化時,要求其父類全部都已經初始化完畢了.
示例二
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
}
}
interface MyParent5{
public static Thread thread = new Thread(){
{
//實例化代碼塊
System.out.println("MyParent 5 invoked ");
}
};
}
interface MyChild5 extends MyParent5{
public static final int b = new Random().nextInt(4);
}
輸出:
0
在初始化一個接口時,並不會先初始化他的父接口
示例三
class MyParent2{
public static String str = "hello world";
static {
System.out.println("MyParent2 static block");
}
}
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
想必你能很快的猜出答案,這屬於對MyParent2
的主動使用,所以輸出:
MyParent2 static block
hello world
但是如果我們將str
改爲常量會輸出什麼?
class MyParent2{
public static final String str = "hello world";
static {
System.out.println("MyParent2 static block");
}
}
輸出:
hello world
這裏並沒有輸出 MyParent2 static block
,沒道理啊,MyParent2.str
屬於對MyParent2
的主動使用啊。
這是因爲常量在編譯階段會存入到調用這個常量的方法所在的類的常量池中,本質上,調用類並沒有直接引用到定義常量的類,因此並不會觸發定義常量的類的初始化。
可以明顯看到,該Class
文件並沒有MyParent2
的任何信息.
示例四
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3{
public static final String str = UUID.randomUUID().toString();
static{
System.out.println("MyParent3 static code");
}
}
輸出:
MyParent3 static code
4ef5aa3a-6773-4678-858d-08e8e108d742
當一個常量的值並非編譯期間可以確定的,那麼其值就不會被放到調用類的常量池中,這時在程序運行時,會導致主動使用這個常量所在的類,導致這個類被初始化。
1.3.4 類加載器準備階段和初始化階段的意義
public class MyTest6 {
public static void main(String[] args) {
Singleton.getInstance();
System.out.println(Singleton.counter1);
System.out.println(Singleton.counter2);
}
}
class Singleton{
public static int counter1;
public static int counter2 = 0;
public static Singleton singleton = new Singleton();
private Singleton(){
counter1++;
counter2++;
System.out.println("靜態代碼塊 counter1: " + counter1);
System.out.println("靜態代碼塊 counter2: " + counter2);
}
public static Singleton getInstance(){
return singleton;
}
}
輸出:
靜態代碼塊 counter1: 1
靜態代碼塊 counter2: 1
1
1
這是因爲當調用 getInstance()
時,屬於對Singleton
類的主動使用,所以會進行初始化。
在初始化前counter1
、counter2
、singleton
的值分別在準備階段被賦值爲0
、0
和null
。然後開始至上向下初始化,先對counter1
初始化,因爲並沒有進行賦值,所以使用默認值。然後對counter2
,這裏將0
賦值給counter2
。當初始化singleton
時,調用構造方法,對初始化後的counter1
和counter2
進行遞增操作。
如果我們更改counter2
的順序,輸出會如何?
class Singleton{
public static int counter1;
public static Singleton singleton = new Singleton();
private Singleton(){
counter1++;
counter2++;
System.out.println("靜態代碼塊 counter1: " + counter1);
System.out.println("靜態代碼塊 counter2: " + counter2);
}
public static int counter2 = 0;
public static Singleton getInstance(){
return singleton;
}
}
輸出:
靜態代碼塊 counter1: 1
靜態代碼塊 counter2: 1
1
0
答案是不是有點出乎你的意料。同樣的初始化階段和之前一樣,接着開始初始化階段。counter1
依然使用準備階段的默認值,接着是對singleton
的初始化(因爲初始化是自上而下的)。進入其構造方法,首先執行counter1++
操作(此時counter1
使用的是初始化後的值),counter1 = 1
;接着進行counter2++
操作(注意這裏使用的是counter2
準備階段的值),counter2 = 1
。
注意構造方法結束並不意味着初始化階段結束,因爲counter2
還沒有被初始化。
接着開始進行counter2
的初始化,counter2
由準備階段的1
變爲0
。
準備階段的重要意義:如果類變量還未初始化,對類變量進行類似++
的操作,沒有默認值是不是會報錯。
2. 類加載器
通過一個類的全限定名來獲取描述此類的二進制字節流,以便讓應用程序自己決定如何去獲取所需要的類,實現這個動作的代碼模塊成爲"類加載器"。
2.1 JDK自帶類加載器的分類
2.1.1 啓動類加載器(Bootstrap ClassLoader)
這個類加載器負責將存在在<JAVA_HOME>\lib
目錄中的,或者被-Xbootclasspath
參數所指定的路徑,並且能夠被虛擬機識別的類庫(如rt.jar
)加載到虛擬機內存中。
2.1.2 擴展類加載器(Extension ClassLoader)
這個類加載器由sun.misc.Launcher$ExtClassLoader
實現,負責加載<JAVA_HOME>\lib\ext
目錄中的,或者被java.ext.dirs
系統變量所指定的路徑中的所有類庫。
2.1.3 應用程序類加載器(Application ClassLoader)
這個類加載器由 sun.misc.Launcher$AppClassLoader
實現,負責加載用戶類路徑(ClassPath
) 上所指定的類庫,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是默認的類加載器。
2.2 雙親委託模型
雙親委派模型可以理解爲類加載時,類加載被調用順序一種機制。
上圖所展示的類加載器之間的關係層次如上所示,該關係成爲類加載器的雙親委派模型。需要注意的是,以上類加載器直接的關係並非繼承。可觀察繼承圖如下所示:
可以明顯看到APPClassLoader
以及ExtClassLoader
都是繼承自URLClassLoader
。同時我們也可以看到並沒有BootstrapClassLoader
,這是因爲BootstrapClassLoader
是由C++
編寫而成的,與Java
的類並沒有任何關係。同樣的,我們也無法獲取該類的實例。
獲取BootstrapClassLoader
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("java.lang.String");
System.out.println(clazz);
System.out.println(clazz.getClassLoader());
}
輸出如下所示
class java.lang.String
null
可以觀察到,如果我們獲取BootstrapClassLoader
時,返回的是null
。
獲取AppClassLoader
class C{
public static void main(String[] args) throws ClassNotFoundException {
Class<?> c = Class.forName("com.wangzhao.jvm.classloader.C");
System.out.println(c);
System.out.println(c.getClassLoader());
}
}
輸出如下所示
class com.jvm.classloader.C
sun.misc.Launcher$AppClassLoader@b4aac2
可以觀察到,我們自定義類是使用AppClassLoader
進行加載的,其屬於Launcher
類下的一個內部類。
2.2.1 工作流程
如果一個類加載器收到了類加載的請求,它首先不會嘗試加載這個類,而是將請求委託給其父類。直到沒有父類爲止,否則一直委託給父類。接着父類開始再其資源路徑下嘗試加載這個類,如果加載不到。那麼回到子類去加載,如果最後的子類也不能加載到,那麼程序拋出異常。
類加載過程的代碼如下
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 首先檢查這個類有沒有被加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果存在父加載器,則讓父加載器去加載這個類
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父加載器爲null的話,則使用啓動類加載器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果一直到啓動類加載器,該類依然沒有被加載
// 那麼在該類加載器對應的路徑下去尋找,如果找不到,再去子加載器的路徑尋找
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;
}
}
}
首先可以明顯看出,類加載的過程與上面的時序圖過程是相同的。
其次,我們在書寫單例模式的時候,有如下一種方式
public class Singleton {
public static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
我們在書寫單例模式的時候,都聽過類加載的過程是線程安全的,但是不知道你有沒有看過其源碼,爲什麼是線程安全的?因爲類加載的過程被synchronized
所修飾。
2.2.2 爲什麼使用雙親委派模型
類加載器的作用都是對Class
文件進行加載,那麼爲什麼不直接使用最底層那個類加載器去加載不就可以了嗎?
一個顯而易見的好處是,Java
類隨着他的類加載器一起具備了一種帶有優先級的層次關係。
以 java.lang.Object
爲例,最終是由BootstrapClassLoader
進行的加載,所以Object
類在程序的各種類加載器都是同一個類。如果沒有雙親委派機制的話,用戶自己編寫了一個稱爲java.lang.Object
的類,那麼程序中將會出現多個不同的Object
類,程序將會很混亂。
如下代碼所示
public class Object {
}
class ObjectClassLoaderTest{
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("java.lang.Object");
System.out.println(clazz);
}
}
程序運行後拋出異常信息如下所示:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
雙親委派機制保證了一個類在程序中是由一個類加載器進行加載,且這種加載是具有優先級的。
2.3 獲取類加載器的方式
- 獲得當前類的類加載器
clazz.getClassLoader()
- 獲得當前線程的上下文類加載器
Thread.currentThread().getContextClassLoader()
- 獲得系統的ClassLoader
ClassLoader.getSystemClassLoader()
2.4 類加載器加載類與反射加載類的區別
public class MyTest8 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> clazz = loader.loadClass("com.wangzhao.jvm.classloader.CL");
System.out.println(clazz);
System.out.println("=============");
clazz = Class.forName("com.wangzhao.jvm.classloader.CL");
System.out.println(clazz);
}
}
輸出
class com.wangzhao.jvm.classloader.CL
=============
CL static block
class com.wangzhao.jvm.classloader.CL
通過上面的輸出可以看到,類加載器加載類並不是對類的主動使用,所以不會初始化類。
2.5 自定義類加載器
2.5.1 方式一
public class MyClassLoader extends ClassLoader{
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
System.out.println(fileName);
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
}
2.5.2 方式二
public class MyTest11 extends ClassLoader {
private String classLoaderName;
private static final String FILE_EXTENSION = ".class";
public MyTest11(String classLoaderName){
super(); // 將系統類加載器設置爲父加載器
this.classLoaderName = classLoaderName;
}
public MyTest11(ClassLoader parent,String classLoaderName){
super(parent); // 顯式指定父加載器
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return defineClass(name,data,0,data.length);
}
public byte[] loadClassData(String className) throws ClassNotFoundException {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
String fileName = className.replace(".","/")+FILE_EXTENSION;
is = new FileInputStream(new File(fileName));
int len = 0;
while((len = is.read())!=-1){
baos.write(len);
}
data = baos.toByteArray();
System.out.println(123);
return data;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public static void test(ClassLoader classLoader) throws Exception {
System.out.println(classLoader.getParent());
Class<?> clazz = classLoader.loadClass("com.wangzhao.jvm.classloader.MyTest1");
Object obj = clazz.newInstance();
System.out.println(obj);
}
public static void main(String[] args) throws Exception{
MyTest11 loader1 = new MyTest11("loader1");
test(loader1);
}
這裏我們談一下這兩種自定義有何區別,當使用第一種時,我們直接調用我們重寫後的loadClass()
方法。但是對於第二種來說,我們相當與重寫了findClass()
,這種情況下,該方法會在ClassLoader
的loadClass()
中調用。如果其父加載器可以進行類的加載,那麼就不會執行我們重寫後的findClass()
,因爲在前面說過,雙親委派模型並不是繼承關係,這樣的加載方式更符合雙親委派機制。
2.6 類是否相等
例如有如下一段代碼(前提,該項目類路徑下並沒有這個Class
文件),那麼這兩個類加載器加載的這個類相等嗎?
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz);
System.out.println(clazz.hashCode());
Object obj = clazz.newInstance();
MyTest11 loader2 = new MyTest11("loader2");
loader2.setPath("/home/wangzhao/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz2);
System.out.println(clazz2.hashCode());
Object obj2 = clazz2.newInstance();
System.out.println(obj2);
}
輸出如下
findClass
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
findClass
24079912
com.wangzhao.jvm.classloader.MyTest11@14ae5a5
通過輸出我們可以清楚看到這個類執行了我們自定義的findClass
方法,也就是說類加載器是我們的自定義類。但是可以明顯看到這兩個類的hashCode
不同,在類加載器不同的情況下,也就是說這兩個類對象是不同的。
那麼如果我們在項目的類路徑下添加這個文件會輸出什麼呢?
10568834
sun.misc.Launcher$AppClassLoader@b4aac2
10568834
sun.misc.Launcher$AppClassLoader@b4aac2
可以看到,如果類加載器是同一個的話,那麼類對象是相等的。
定義如下類
public class MyPerson {
private MyPerson myPerson;
public void setMyPerson(Object obj){
this.myPerson = (MyPerson)obj;
}
}
執行下面的代碼
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
MyTest11 loader2 = new MyTest11("loader2");
Class<?> clazz1 =loader1.loadClass("com.wangzhao.jvm.classloader.MyPerson");
Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyPerson");
System.out.println(clazz1 == clazz2);
Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();
Method method = clazz1.getMethod("setMyPerson",Object.class);
method.invoke(obj1,obj2);
}
程序正常執行,這是因爲clazz1
和clazz2
都是由AppClassLoader
加載。
對於任意一個類,都需要由加載他的類加載器和這個類本身一同確立其在Java
虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。即比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器實例 加載的前提下才有意義,否則,即使這兩個類來源於同一個Class
文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那麼這兩個類就必定不相等。
2.7 命名空間
- 每個類加載器都有自己的命名空間,命名空間由該類加載器及其父加載器所加載的類共同組成。
- 在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。
- 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。
同樣的在類路徑下沒有該class文件的前提下,運行如下代碼
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz.hashCode());
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
MyTest11 loader2 = new MyTest11(loader1,"loader2");
loader2.setPath("/home/wangzhao/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz2.hashCode());
Object obj2 = clazz2.newInstance();
System.out.println(obj2.getClass().getClassLoader());
}
輸出結果如下
findClass
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
通過輸出可以看到,loader2
加載器並沒有執行findClass()
,即MyTest1
被沒有被loader2
加載。這是因爲loader2
的父加載器loader1
已經加載了MyTest
1,通過雙親委託機制,當loader2
加載MyTest1
時,先讓其父加載器加載。
2.7.1 類加載器命名空間示例
示例一
存在下面這樣一個類
public class MyPerson {
private MyPerson myPerson;
public void setMyPerson(Object obj){
this.myPerson = (MyPerson)obj;
}
}
運行下面的代碼,前提類路徑下存在MyPerson.class文件
public class MyTest14 {
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
MyTest11 loader2 = new MyTest11("loader2");
loader1.setPath("C:\\Users\\25519\\Desktop\\");
loader2.setPath("C:\\Users\\25519\\Desktop\\");
Class<?> clazz1 =loader1.loadClass("com.wangzhao.jvm.classloader.MyPerson");
Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyPerson");
System.out.println(clazz1 == clazz2);
Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();
Method method = clazz1.getMethod("setMyPerson",Object.class);
method.invoke(obj1,obj2);
}
輸出如下
true
如果我們將類路徑下的MyPerson文件刪除後,放到桌面後。繼續運行上面的程序,輸出如下
findClass
findClass
false
Exception in thread "main" java.lang.reflect.InvocationTargetException
Caused by: java.lang.ClassCastException: com.wangzhao.jvm.classloader.MyPerson cannot be cast to com.wangzhao.jvm.classloader.MyPerson
我們分析一下爲什麼會出現這種情況,首先我們沒有刪除類路徑下得MyPerson.class
文件時,MyTest11
的父加載器可以在類路徑下找到Class
文件並加載,所以clazz1
和clazz2
是同一個對象。
而刪除Class
文件後,是由我們自定義的類加載器所加載,但是loader1
和loader2
屬於不同的實例,所以loader1
和loader2
的命名空間不同,所以clazz1
和clazz2
是不同的class對象。當對象實例屬於不同得命名空間時,即使是由同一份Class
文件加載,當進行類型轉換時(非繼承、實現關係)一定會出現類型轉換異常。
示例二
存在如下這些類
public class MyCat {
public MyCat(){
System.out.println("MyCat loaded by : " + this.getClass().getClassLoader());
}
}
public class MySample {
public MySample(){
System.out.println("MySample loaded by : "+this.getClass().getClassLoader());
new MyCat();
}
}
當我們執行如下的代碼時
public class MyTest12 {
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MySample");
System.out.println("class: " + clazz.hashCode());
// 如果註釋掉該行,那麼並不會實例化MySample對象,即MySample構造方法不會被調用
// 因此不會實例化MyCat對象,即沒有對MyCat進行主動使用,這裏就不會加載MyCat Class
// 注意:沒有對類進行主動使用,並不意味着一定不會加載Class文件
Object obj = clazz.newInstance();
}
}
輸出如下
class: 21029277
MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
這是因爲loader1
的父加載器AppCLassLoader
在類路徑下可以找到MySample
的Class
文件,所以MySample
是由AppCLassLoader
所加載,而MyCat
位於MySample
的構造方法中,所以MyCat
是由MySanple
的類加載器或其父加載器去加載,因爲AppClassLoader
可以在類路徑下找到MyCat
的Class
文件,所以可以加載成功。
將MyCat.class
從項目路徑下刪除,放到桌面,保留MySample.class
在類路徑下。執行下面的代碼
public static void main(String[] args) throws Exception {
MyTest11 loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MySample");
System.out.println("class: " + clazz.hashCode());
Object obj = clazz.newInstance();
}
輸出
class: 21029277
MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: com/wangzhao/jvm/classloader/MyCat
通過輸出可以看到,MyCat
加載失敗,這是因爲MySample
由AppClassLoader
加載,所以MyCat也需要由AppClassLoader
加載,但是類路徑下沒有MyCat.class
,所以加載失敗。
將MySample.class
從項目路徑下刪除,放到桌面,保留MyCat.class
在類路徑下,繼續執行上面的代碼,輸出
findClass
class: 21685669
MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
通過輸出可以看到,當加載MySample
時,是由我們自己定義的類加載器加載的。這是因爲我們自定義的類加載器的父加載器不可以加載MySample
,而我們自定義的類加載器可以加載,所以按照我們自定義類加載器的方式去加載。而加載MyCat
時,根據雙親委派機制,先由loader1
的父加載器去加載,而AppClassLoader
可以加載到MyCat
。
修改MyCat
的代碼,如下所示
public class MyCat {
public MyCat() {
System.out.println("MyCat loaded by : " + this.getClass().getClassLoader());
System.out.println("from MyCat : " + MySample.class);
}
}
重新編譯後,在類路徑下保留MyCat.class
和MySample.class
後,輸出如下
class: 21029277
MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
from MyCat : class com.wangzhao.jvm.classloader.MySample
如果將MySample.class
從類路徑中刪除,放到桌面後,輸出如下
findClass
class: 21685669
MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: com/wangzhao/jvm/classloader/MySample
可以看到,獲得MySample的class
對象失敗這是因爲MySample
由loader1
加載,而MyCat
是由其父加載器加載。在父加載器中看不到子加載器加載信息。
修改MySample和MyCat的代碼如下所示
public class MyCat {
public MyCat() {
System.out.println("MyCat loaded by : " + this.getClass().getClassLoader());
//System.out.println("from MyCat : " + MySample.class);
}
}
public class MySample {
public MySample(){
System.out.println("MySample loaded by : "+this.getClass().getClassLoader());
// 由加載MySample的類加載器去加載MyCat
new MyCat();
System.out.println("from Sample : " + MyCat.class);
}
}
重新編譯後運行,輸出如下
class: 21029277
MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
from Sample : class com.wangzhao.jvm.classloader.MyCat
將MySample
從類路徑下刪除,放到桌面後,運行輸出如下
findClass
class: 21685669
MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
from Sample : class com.wangzhao.jvm.classloader.MyCat
MySample
由loader1
加載,MyCat
由AppClassloader
加載。當在loader1
中獲取MyCat
的class
對象時,子加載器可以訪問到父加載器的加載信息,所以可以獲取成功。
2.7.2 命名空間結論
- 同一個命名空間內的類是相互可見的
- 如果兩個加載器之間沒有直接或間接的父子關係,那麼它們各自加載的類是互不可見的
- 子加載器可以訪問到被父加載器加載的類
- 父加載器無法訪問到被子加載器加載的類
2.8 類的卸載
當某個類被加載、連接和初始化後,它的生命週期就開始了。當代表這個類的Class
對象不再被引用,即不可觸及時,Class對象就會結束生命週期,這個類在方法區內的數據也會被卸載,從而結束該類的生命週期。
一個類何時結束生命週期,取決於代表它的Class對象何時結束生命週期。
由Java虛擬機自帶的類加載器所加載的類,在虛擬機的生命週期中,始終不會被卸載。Java
虛擬機自帶的類加載器包括根類加載器、擴展類加載器和系統類加載器。Java
虛擬機自身會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class
對象,因此這些Class
對象始終是可觸及的。
由用戶自定義的類加載器所加載的類是可以被卸載的。
// -XX:+TraceClassUnloading輸出有哪些類被卸載
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyTest11 loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz.hashCode());
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
// 去除引用,讓垃圾回收器回收
loader1 = null;
clazz = null;
obj = null;
System.gc();
loader1 = new MyTest11("loader1");
loader1.setPath("/home/wangzhao/Desktop/");
clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
System.out.println(clazz.hashCode());
obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
}
輸出如下
findClass
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
[Unloading class com.wangzhao.jvm.classloader.MyTest1 0xa43e4228]
findClass
24079912
com.wangzhao.jvm.classloader.MyTest11@14ae5a5
可以看到,findClass
輸出兩次,也就是說Class
對象被卸載了一次。
3. 補充說明
3.1 雙親委託模型的好處
- 確保
Java
核心庫的類型安全。
所有的Java
應用都至少引用java.lang.Object
類,也就是說在運行期,java.lang.Object
這個類會被加載到 Java 虛擬機中;如果這個加載過程是由自定義的類加載器去完成的話,那麼很可能就會在JVM
中存在多個版本的java.lang.Object
類,而且這些類之間還是不兼容的,相互之間不可見的(命名空間在發揮的作用)。
藉助於雙親委託機制,Java
核心類庫中的類的加載工作都是由啓動類加載器來統一完成的,從而確保了Java
應用程序所使用的都是同一個版本的Java
核心類庫,他們之間是相互兼容的。
- 不同的類加載器可以爲相同名稱的類創建額外的命名空間。
相同名稱的類可以並存在Java虛擬機中,只需要用不同的類加載器來加載他們即可。不同類加載器所加載的類之間是不兼容的,這就相當於在Java虛擬機內部創建了一個又一個相互隔離的Java類空間,這類技術在很多框架中都得到了應用。
3.2 擴展類加載器要點分析
執行下面的代碼
public class MyTest15 {
public static void main(String[] args) {
System.out.println(MyTest15.class.getClassLoader());
System.out.println(MyTest1.class.getClassLoader());
}
}
相信你能很快知道輸出結果,輸出如下
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
在上文中說到過,我們可以通過更改對應加載器的加載路徑,可以改變類加載器。那麼,如果我們將拓展類加載器的加載路徑,改爲我們當前的目錄,輸出結果會是什麼
通過上面的輸出可以看到,上面的兩個類依然是由AppClassLoader加載。在上一篇文章中說過,如果擴展類加載器其類路徑下的class文件,需要是類庫的形式。所以先進行下面的打包
此時可以看到MyTest1
是由擴展類加載器加載
3.3 啓動類加載器分析
在Oracle
的Hotspot
實現中,系統屬性sun.boot.class.path
如果修改錯了,則運行會出錯,提示如下信息
內建於JVM中
的啓動類加載器會加載java.lang.ClassLoader
以及其他的Java
平臺類,當JVM
啓動時,一塊特殊的機器碼會運行,它會加載擴展類加載器與系統類加載器,這塊特殊的機器碼叫做啓動類加載器(Bootstrap)。
啓動類加載器並不是Java
類,而其他的加載器則是Java
類,啓動類加載器時特定於平臺的機器指令,它負責開啓整個加載過程。
所有類加載器(除了啓動類加載器)都被實現爲Java
類。不過,總歸由一個組件加載第一個Java
類加載器,從而讓整個加載過程能夠順利進行下去,加載第一個純Java類加載器就是啓動類加載器的職責。
啓動類加載器還會負責加載供JRE正常運行所需要的基本組件,包括java.unit
與java.lang
包中的類等。
public static void main(String[] args) {
System.out.println(ClassLoader.class.getClassLoader());
// 擴展類加載器與系統類加載器也是由啓動類加載器所加載的
System.out.println(Launcher.class.getClassLoader());
System.out.println(ClassLoader.getSystemClassLoader());
}
輸出如下
null
null
sun.misc.Launcher$AppClassLoader@18b4aac2
可以看到ClasssLoader
以及Launcher
的類加載器都是啓動類加載器,因爲擴展類加載器與系統類加載器屬於Launcher
內部類,我們不能直接訪問,而Launcher
的類加載會嘗試加載其成員。同時可以看到系統類加載器默認是AppClassLoader
。
3.3 更改系統類加載器
我們知道系統類加載器默認是AppClassLoader
,系統類加載器是默認的自定義加載器的父加載器。
運行下面的代碼
public class MyTest16 {
public static void main(String[] args) {
System.out.println(System.getProperty("java.system.class.loader"));
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(new MyTest11("loader").getParent());
}
}
輸出如下
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
將系統類加載器改爲我們自己定義的加載器出錯,這是因爲我們需要在自定義類加載器中添加一個ClassLoader
的構造器,如下所示:
public MyTest11(ClassLoader parent){
super(parent);
}
可以看到自定義ClassLoader的類加載器的父加載器不再是AppClassLoader
。
在我們更改默認的系統類加載器後,其是由AppClassLoader
加載。
3.4 Launcher源碼分析
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
// 用於確認系統類加載器有沒有被賦值
private static boolean sclSet;
// 系統類加載器
private static ClassLoader scl;
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
// 如果沒有被賦值,但是系統類加載器卻不爲空,不符合邏輯,所以出錯
throw new IllegalStateException("recursive invocation");
// 返回一個Launcer的實例
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 將AppClassLoader設置爲系統類加載器
scl = l.getClassLoader();
try {
// 系統類加載器需不需要進行改變
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
// 表示系統類加載器設置完畢
sclSet = true;
}
}
獲取Launcher
實例
public Launcher() {
// 擴展類加載器
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 獲取AppClassLoader時,將ExtClassLoader傳入,這是爲了將loader的父加載器設置爲ExtClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 將剛創建好的應用類加載器設置爲當前線程的上下文類加載器
Thread.currentThread().setContextClassLoader(this.loader);
// 安全管理器相關設置
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
獲取ExtClassLoader
實例
static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
獲取系統的java.ext.dirs
路徑下的所有文件
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
通過代碼,我麼便可以知道,爲什麼擴展類的加載路徑是java.ext.dirs
。
在Launcher的構造器中創建好之後,緊接着開始創建AppClassLoader
。創建過程如下
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
通過代碼,可以看到AppClassLoader
的加載路徑同樣讀取java.class.path
下的所有文件。
3.5 自定義系統類加載器源碼分析
class SystemClassLoaderAction
implements PrivilegedExceptionAction<ClassLoader> {
private ClassLoader parent;
SystemClassLoaderAction(ClassLoader parent) {
this.parent = parent;
}
public ClassLoader run() throws Exception {
String cls = System.getProperty("java.system.class.loader");
if (cls == null) {
// 如果我們沒有設置過java.system.class.loader,那麼系統類加載器爲AppClassLoader
return parent;
}
// 通過反射調用一個帶ClassLoader參數的構造方法
// 這就是爲什麼我們自定義類加載器設置爲SystemClassLoader時,需要有一個ClassLoader參數的構造方法
Constructor<?> ctor = Class.forName(cls, true, parent)
.getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
// 系統類加載器的父加載器設置爲parent
ClassLoader sys = (ClassLoader) ctor.newInstance(
new Object[] { parent });
// 將用戶自定義的類加載器設置爲上下文類加載器
Thread.currentThread().setContextClassLoader(sys);
return sys;
}
}
3.6 forName方法剖析
// name 全限定類名
// initialize 是否進行初始化
// loader 加載指定類的類加載器
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 獲取調用forName()方法的類的class對象
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
// 獲取caller的classLoader對象
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
該方法的作用是返回使用給定的類加載器加載給定字符串名稱的類或接口的Class
對象。
Class.forName(“Foo”) = Class.forName(“Foo”, true, this.getClass().getClassLoader())
3.7 線程上下文類加載器
public class MyTest17 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getContextClassLoader());
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(Thread.class.getClassLoader());
// null
}
}
- 當前類加載器
每個類都會使用自己的類加載器(即加載自身的類加載器)來去加載其他類(指的是所依賴的類),如果ClassX
引用了ClassY
,那麼ClassX
的類加載器就會去加載ClassY
(前提是ClassY
尚未被加載)。
- 線程上下文類加載器
線程上下文類加載器從JDK1.2
開始引入的,類Thread
中的getContextClassLoader()
與setContextClassLoader(ClassLoader cl)
分別用來獲取和設置上下文類加載器。
如果沒有通過setContextClassLoader(cl)
設置的話,線程將繼承其父線程的上下文類加載器。
Java
應用運行時的初始化線程的上下文類加載器就是系統類加載器。在線程中運行的代碼可以通過該類加載器來加載類於資源。
線程上下文類加載器的重要性:SPI(Service Provider Interface)
-
父
ClassLoader
可以使用當前線程Thread.currentThread().getContextClassLoader()
所指定的classloader
加載的類。這就改變了父Classloader
不能使用子ClassLoader
或是其他沒有直接父子關係的classLoader
加載的類的情況。即改變了雙親委託模型。 -
在雙親委託模型下,類加載器是由下至上的,即下層的類加載器會委託上層進行加載。但是對於
SPI
來說,有些接口時Java
核心庫所提供的,而Java
核心庫是由啓動類加載器來加載的,而這些接口的實現卻來自於不同的jar
包(廠商提供),Java
的啓動類加載器是不會加載其他來源的jar包,這樣傳統的雙親委託模型就無法滿足SPI
的要求(這樣導致接是啓動類加載,而實現是由系統類加載器或應用類加載器加載)。而通過給當前線程設置上下文類加載器,就可以由設置的上下文類加載器來實現對於接口實現類的加載。
以JDBC
爲例,JDBC
的接口是啓動類加載器加載,但是JDBC
的接口的實現卻是由數據庫廠商所提供。因爲JDBC
的接口和其實現存在依賴關係,而其實現的jar包被我們放到了classpath
,這樣便導致了啓動類加載器無法加載其實現,從而我們無法使用JDBC
,所以傳統的雙親委託機制便失效了。
public class MyTest18 implements Runnable{
private Thread thread;
public MyTest18(){
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
ClassLoader classLoader = this.thread.getContextClassLoader();
this.thread.setContextClassLoader(classLoader);
System.out.println("Class: " + classLoader.getClass());
System.out.println("Parent: " + classLoader.getParent().getClass());
}
public static void main(String[] args) {
new MyTest18();
}
}
該程序的輸出通過前面的分析,想必很快能知道答案,輸出如下
Class: class sun.misc.Launcher$AppClassLoader
Parent: class sun.misc.Launcher$ExtClassLoader
3.7.1 線程上下文類加載器的使用
線程上下文類加載的一般使用模式 (獲取 - 使用 - 還原)
僞代碼如下
try{
// targetThreadContextClassLoader將要使用的ClassLoader設置爲線程上下文類加載器
Thread.currentThread().setContextClassLoader(targetThreadContextClassLoader);
doSomething();
}finally{
Thread.currentThread().setClassLoader(classLoader);
}
doSomething()
裏面調用了Thread.currentThread.getContextClassLoader()
,獲取當前線程的上下文類加載器做某些事情。如果一個類由類加載器A
加載,那麼這個類的類加載器會加載這個類的依賴類(前提依賴類沒有被加載)
ContextClassLoader
的作用是爲了破壞Java的類加載器委託機制。
當高層提供了統一的接口讓低層去實現,同時又要在高層加載(或實例化)低層的類時,就必須要通過線程上下文類加載器來幫助高層的ClassLoader
找到並加載該類。