類加載機制總結
一.類加載器基本概念
類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之後就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的 newInstance()方法就可以創建出該類的一個對象。實際的情況可能更加複雜,比如 Java 字節代碼可能是通過工具動態生成的,也可能是通過網絡下載的。如下圖。
其實可以一句話來解釋:類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class對象,用來封裝類在方法區內的數據結構。
二. 在什麼時候纔會啓動類加載器?
其實,類加載器並不需要等到某個類被“首次主動使用”時再加載它,JVM規範允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。
其中加載(除了自定義加載)+鏈接的過程是完全由jvm負責的,什麼時候要對類進行初始化工作(加載+鏈接在此之前已經完成了),jvm有嚴格的規定(四種情況):
1.遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時,加入類還沒進行初始化,則馬上對其進行初始化工作。其實就是3種情況:用new實例化一個類時、讀取或者設置類的靜態字段時(不包括被final修飾的靜態字段,因爲他們已經被塞進常量池了)、以及執行靜態方法的時候。
2.使用java.lang.reflect.*的方法對類進行反射調用的時候,如果類還沒有進行過初始化,馬上對其進行。
3.初始化一個類的時候,如果他的父親還沒有被初始化,則先去初始化其父親。
4.當jvm啓動時,用戶指定一個要執行的主類(包含static void main(String[] args)的那個類),則jvm會先去初始化這個類。
以上4種預處理稱爲對一個類進行主動的引用,其餘的其他情況,稱爲被動引用,都不會觸發類的初始化。
三. 從哪個地方去加載.class文件
在這裏進行一個簡單的分類。例舉了5個來源
(1)本地磁盤
(2)網上加載.class文件(Applet)
(3)從數據庫中
(4)壓縮文件中(ZAR,jar等)
(5)從其他文件生成的(JSP應用)
有了這個認識之後,下面就開始講講,類加載機制了。首先看的就是類加載機制的過程。
四. 類加載的過程
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。它們的順序如下圖所示:
其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,因爲這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。
下面就一個一個去分析一下這幾個過程。
1)、加載
”加載“是”類加機制”的第一個過程,在加載階段,虛擬機主要完成三件事:
(1)通過一個類的全限定名來獲取其定義的二進制字節流
(2)將這個字節流所代表的的靜態存儲結構轉化爲方法區的運行時數據結構
(3)在堆中生成一個代表這個類的Class對象,作爲方法區中這些數據的訪問入口。
相對於類加載的其他階段而言,加載階段是可控性最強的階段,因爲程序員可以使用系統的類加載器加載,還可以使用自己的類加載器加載
2)、驗證
驗證的主要作用就是確保被加載的類的正確性。也是連接階段的第一步。說白了也就是我們加載好的.class文件不能對我們的虛擬機有危害,所以先檢測驗證一下。他主要是完成四個階段的驗證:
(1)文件格式的驗證:驗證.class文件字節流是否符合class文件的格式的規範,並且能夠被當前版本的虛擬機處理。這裏面主要對魔數、主版本號、常量池等等的校驗(魔數、主版本號都是.class文件裏面包含的數據信息、在這裏可以不用理解)。
(2)元數據驗證:主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合java語言規範的要求,比如說驗證這個類是不是有父類,類中的字段方法是不是和父類衝突等等。
(3)字節碼驗證:這是整個驗證過程最複雜的階段,主要是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。在元數據驗證階段對數據類型做出驗證後,這個階段主要對類的方法做出分析,保證類的方法在運行時不會做出威海虛擬機安全的事。
(4)符號引用驗證:它是驗證的最後一個階段,發生在虛擬機將符號引用轉化爲直接引用的時候。主要是對類自身以外的信息進行校驗。目的是確保解析動作能夠完成。
對整個類加載機制而言,驗證階段是一個很重要但是非必需的階段,如果我們的代碼能夠確保沒有問題,那麼我們就沒有必要去驗證,畢竟驗證需要花費一定的的時間。當然我們可以使用-Xverfity:none來關閉大部分的驗證。
3)、準備
準備階段主要爲類變量分配內存並設置初始值。這些內存都在方法區分配。在這個階段我們只需要注意兩點就好了,也就是類變量和初始值兩個關鍵詞:
(1)類變量(static)會分配內存,但是實例變量不會,實例變量主要隨着對象的實例化一塊分配到java堆中,
(2)這裏的初始值指的是數據類型默認值,而不是代碼中被顯示賦予的值。比如
public static int value = 1; //在這裏準備階段過後的value值爲0,而不是1。賦值爲1的動作在初始化階段。
當然還有其他的默認值。
注意,在上面value是被static所修飾的準備階段之後是0,但是如果同時被final和static修飾準備階段之後就是1了。我們可以理解爲static final在編譯器就將結果放入調用它的類的常量池中了。
4)、解析
解析階段主要是虛擬機將常量池中的符號引用轉化爲直接引用的過程。什麼是符號應用和直接引用呢?
符號引用:以一組符號來描述所引用的目標,可以是任何形式的字面量,只要是能無歧義的定位到目標就好,就好比在班級中,老師可以用張三來代表你,也可以用你的學號來代表你,但無論任何方式這些都只是一個代號(符號),這個代號指向你(符號引用)
直接引用:直接引用是可以指向目標的指針、相對偏移量或者是一個能直接或間接定位到目標的句柄。和虛擬機實現的內存有關,不同的虛擬機直接引用一般不同。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
5)、初始化
這是類加載機制的最後一步,在這個階段,java程序代碼纔開始真正執行。我們知道,在準備階段已經爲類變量賦過一次值。在初始化階端,程序員可以根據自己的需求來賦值了。一句話描述這個階段就是執行類構造器< clinit >()方法的過程。
在初始化階段,主要爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:
①聲明類變量是指定初始值
②使用靜態代碼塊爲類變量指定初始值
JVM初始化步驟
1、假如這個類還沒有被加載和連接,則程序先加載並連接該類
2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3、假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化時機:只有當對類的主動使用的時候纔會導致類的初始化,類的主動使用包括以下幾種:
(1)、創建類的實例,也就是new的方式
(2)、訪問某個類或接口的靜態變量,或者對該靜態變量賦值
調用類的靜態方法
(3)、反射(如 Class.forName(“com.Test”))
(4)、初始化某個類的子類,則其父類也會被初始化
(5)、Java虛擬機啓動時被標明爲啓動類的類( JavaTest),直接使用 java.exe命令來運行某個主類
好了,到目前爲止就是類加載機制的整個過程,但是還有一個重要的概念,那就是類加載器。在加載階段其實我們提到過類加載器,說是在後面詳細說,在這就好好地介紹一下類加載器。
5. 類加載器
分類:
系統級別的類加載器:
Bootstrap ClassLoader(啓動類加載器)
Extention ClassLoader (擴展類加載器)
Appclass Loader (系統類加載器)
用戶級別的類加載器
自定義類加載器(繼承ClassLoader)
系統級別的類加載器:
(1)Bootstrap ClassLoader :最頂層的加載類,主要加載核心類庫,也就是我們環境變量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通過啓動jvm時指定-Xbootclasspath和路徑來改變Bootstrap ClassLoader的加載目錄。比如java -Xbootclasspath/a:path被指定的文件追加到默認的bootstrap路徑中。我們可以打開我的電腦,在上面的目錄下查看,看看這些jar包是不是存在於這個目錄。
(2)Extention ClassLoader :擴展的類加載器,加載目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件。還可以加載-D java.ext.dirs選項指定的目錄。
(3)Appclass Loader:也稱爲SystemAppClass。 加載當前應用的classpath的所有類。我們看到java爲我們提供了三個類加載器,應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。這三種類加載器的加載順序是什麼呢?
Bootstrap ClassLoader > Extention ClassLoader > Appclass Loader
一張圖來看一下他們的層次關係
代碼演示:
Bootstrap ClassLoader(啓動類加載器)
代碼:
public static void bootClassLoaderLoadingPath(){
String bootClassLoaderLoadingPath = System.getProperty("sun.boot.class.path");
List<String> asList = Arrays.asList(bootClassLoaderLoadingPath.split(";"));
for (String s : asList) {
System.out.println("【啓動類加載器====加載目錄】"+s);
}
}
輸出:
Extention ClassLoader (擴展類加載器)職責
代碼演示:
public static void extClassLoaderLoadingPath(){
String bootClassLoaderLoadingPath = System.getProperty("java.ext.dirs");
List<String> asList = Arrays.asList(bootClassLoaderLoadingPath.split(";"));
for (String s : asList) {
System.out.println("【拓展類加載器====加載目錄】"+s);
}
}
輸出:
Appclass Loader(系統類加載器)職責
代碼演示:
public static void appClassLoaderLoadingPath(){
String appClassLoaderLoadingPath = System.getProperty("java.class.path");
List<String> asList = Arrays.asList(appClassLoaderLoadingPath.split(";"));
for (String s : asList) {
System.out.println("【系統類加載器====加載目錄】"+s);
}
}
輸出:
3)雙親委派原則
工作流程是: 當一個類加載器收到類加載任務,會先交給其父類加載器去完成,因此最終加載任務都會傳遞到頂層的啓動類加載器,只有當父類加載器無法完成加載任務時,纔會嘗試執行加載任務。
採用雙親委派的一個好處是比如加載位於rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object對象。雙親委派原則歸納一下就是:
a.可以避免重複加載,父類已經加載了,子類就不需要再次加載
b.更加安全,很好的解決了各個類加載器的基礎類的統一問題,如果不使用該種方式,那麼用戶可以隨意定義類加載器來加載核心api,會帶來相關隱患。
4)、自定義類加載器
在這一部分第一小節中,我們提到了java系統爲我們提供的三種類加載器,還給出了他們的層次關係圖,最下面就是自定義類加載器,那麼我們如何自己定義類加載器呢?這主要有兩種方式
(1)遵守雙親委派模型:繼承ClassLoader,重寫findClass()方法。
(2)破壞雙親委派模型:繼承ClassLoader,重寫loadClass()方法。 通常我們推薦採用第一種方法自定義類加載器,最大程度上的遵守雙親委派模型。
我們看一下實現步驟
(1)創建一個類繼承ClassLoader抽象類
(2)重寫findClass()方法
(3)在findClass()方法中調用defineClass()
代碼演示:
被加載的類:
/**
* @ClassName Sun
* @Description TODO 被加載的類
* @Author 胡澤
* @Date 2020/2/25 21:32
* @Version 1.0
*/
public class Sun {
static{
System.out.println("Hello,HZ ,Sun 已經初始化了");
}
}
自定義classloader 類
package com.company;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* @ClassName SunClassloader
* @Description TODO 自定義classloader
* @Author 胡澤
* @Date 2020/2/25 21:33
* @Version 1.0
*/
public class SunClassloader extends ClassLoader {
/**
* class文件的路徑
*/
private String path;
public SunClassloader(String path) {
this.path = path;
}
public SunClassloader() {
}
public SunClassloader(ClassLoader parent) {
super(parent);
}
//defineClass 通常是和findClass 方法一起使用的,我們通過覆蓋ClassLoader父類的findClass 方法來實現類的加載規則,
// 從而取得要加載類的字節碼,然後調用defineClass方法生成類的Class 對象,如果你想在類被加載到JVM中時就被鏈接,
// 那麼可以接着調用另一個 resolveClass 方法,當然你也可以選擇讓JVM來解決什麼時候才鏈接這個類。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("hello i am hz‘s loader.");
String classPath = path + ".class";
try (InputStream in = new FileInputStream(classPath)) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
byte[] byteArray = out.toByteArray();
//defineClass 方法用來將 字節流解析成 JVM 能夠識別的 Class 對象
return defineClass(null,byteArray, 0, byteArray.length);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("hello i am hz‘s loader.");
return null;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
System.out.println("hello i am hz‘s loader.");
return super.loadClass(name);
}
}
實現類加載:
public static void main(String[] args) throws Exception {
SunClassloader sunClazz = new SunClassloader("E:\\classloader\\src\\com\\company\\Sun");
Class<?> clazz = sunClazz.findClass("Sun");
clazz.newInstance();
}
6.ClassLoader 核心方法
如下圖
從圖可以看出頂層的類加載器是ClassLoader類,它是一個抽象類,其後所有的類加載器都繼承自ClassLoader(不包括啓動類加載器),這裏我們主要介紹ClassLoader中幾個比較重要的方法。
- loadClass(String)
該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2之後不再建議用戶重寫但用戶可以直接調用該方法,loadClass()方法是ClassLoader類自己實現的,該方法中的邏輯就是雙親委派模式的實現,其源碼如下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數代表是否生成class對象的同時進行解析相關操作。
主要源碼:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先從緩存查找該class對象,找到就不用重新加載
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
// 如果都沒有找到,則通過自定義實現的findClass去查找並加載
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()方法去加載(關於findClass()稍後會進一步介紹)。從loadClass實現也可以知道如果不想重新定義加載類的規則,也沒有複雜的邏輯,只想在運行時加載自己指定的類,那麼我們可以直接使用this.getClass().getClassLoder.loadClass("className"),這樣就可以直接調用ClassLoader的loadClass方法獲取到class對象。
7.ClassLoader 核心方法舉例
(1)重寫findClass方法:
代碼舉例:
package com.company;
import java.io.*;
/**
* @ClassName MyClassLoader
* @Description TODO
* @Author 胡澤
* @Date 2020/3/3 12:24
* @Version 1.0
*/
public class MyClassLoader extends ClassLoader {
private String classLoaderName;
private String fileExtension = ".class";
public MyClassLoader(String classLoaderName) {
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader parent, String classLoaderName) {
super(parent);
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
return defineClass(null, classData, 0, classData.length);
}
public byte[] getClassData(String className) {
byte[] bytes = null;
InputStream is = null;
ByteArrayOutputStream bos = null;
try {
is = new FileInputStream(new File(className, fileExtension));
bos = new ByteArrayOutputStream();
int ch = 0;
while ((ch = is.read()) != -1) {
bos.write(ch);
}
bytes = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return bytes;
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyClassLoader myClassLoader=new MyClassLoader("MyLoader");
Class<?> loadClass = myClassLoader.loadClass("com.company.People");
Object instance = loadClass.newInstance();
System.out.println(instance.getClass().getClassLoader());
//此時打印的類加載器任然是系統類加載器
//原因:系統類加載器加載的是項目中的類,People就是項目中的類,
//按照雙親委託模型:MyClassLoader,將任務委託給父加載器(系統類加載器),父加載器發現可以加載,就加載類了,並不需要在給子類去加載。
//故MyClassLoader的findClass方法不會走
}
}
輸出:
//同樣的道理,只要加載的類不是項目中的類,findClass方法就會走,因爲父加載器都不會加載。實例如下
package com.company;
import java.io.*;
/**
* @ClassName MyClassLoader2
* @Description TODO
* @Author 胡澤
* @Date 2020/3/3 12:29
* @Version 1.0
*/
public class MyClassLoader2 extends ClassLoader{
private String classLoaderName;
private String fileExtension = ".class";
private String path;
public MyClassLoader2(String classLoaderName) {
super();
this.classLoaderName = classLoaderName;
}
public void setPath(String path) {
this.path = path;
}
public MyClassLoader2(ClassLoader parent, String classLoaderName) {
super(parent);
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("findClass invoke: "+name);
System.out.println("class loader name: "+classLoaderName);
byte[] classData = getClassData(name);
return defineClass(null, classData, 0, classData.length);
}
public byte[] getClassData(String className) {
byte[] bytes = null;
InputStream is = null;
ByteArrayOutputStream bos = null;
className=className.replaceAll("\\.","\\\\");
try {
File file=new File(this.path,className+fileExtension);
is = new FileInputStream(file);
bos = new ByteArrayOutputStream();
int ch = 0;
while ((ch = is.read()) != -1) {
bos.write(ch);
}
bytes = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return bytes;
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyClassLoader2 myTest=new MyClassLoader2("MyLoader");
myTest.setPath("C:\\Users\\14255\\Desktop");
Class<?> loadClass = myTest.loadClass("com.People");
Object instance = loadClass.newInstance();
System.out.println(instance.getClass().getClassLoader());
System.out.println(instance);
//打印的類加載器是自定義的MyClassLoader2
}
}
輸出:
以上例子充分體現了雙親委派原則。
- 重寫loadClass方法
代碼舉例:
package com.company;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader3 extends ClassLoader {
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
try {
// 這個getClassInputStream根據情況實現
InputStream is = getClassInputStream(name);
if (is == null) {
return super.loadClass(name);
}
byte[] bt = new byte[is.available()];
is.read(bt);
return defineClass(name, bt, 0, bt.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class " + name + " not found.");
}
}
private InputStream getClassInputStream(String name) {
String filename = name.replace('.', '/') + ".class";
InputStream is = getClass().getResourceAsStream(filename);
return is;
}
}
輸出:
(3)調用順序