markdown 語法
要點:
1、類載入機制的原理
2、程式初始化的順序
3、類載入的代理模式(雙親委託機制)
一、類載入機制
JVM把class檔案載入到記憶體,並對資料進行校驗、準備、解析、初始化,最終形成JVM可以直接使用的Java型別的過程。‘
類載入全過程1、載入
將class位元組碼檔案載入到記憶體中,並將這些資料轉換成方法區中的執行時資料(靜態變數、靜態程式碼塊、常量池等),在堆中生成一個Class類物件代表這個類(反射原理),作為方法區類資料的訪問入口。
類的載入原理2、連結
將Java類的二進位制程式碼合併到JVM的執行狀態之中。
• 驗證
確保載入的類資訊符合JVM規範,沒有安全方面的問題。
• 準備
正式為類變數(static變數)分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配。注意此時的設定初始值為預設值,具體賦值在初始化階段完成。
• 解析
虛擬機器常量池內的符號引用替換為直接引用(地址引用)的過程。
3、初始化
初始化階段是執行類構造器<clinit>()方法的過程。類構造器<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static塊)中的語句合併產生的。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化、則需要先初始化其父類。
- 虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確加鎖和同步。
二、Java程式初始化順序
1、父類的靜態變數
2、父類的靜態程式碼塊
3、子類的靜態變數
4、子類的靜態程式碼塊
5、父類的非靜態變數
6、父類的非靜態程式碼塊
7、父類的構造方法
8、子類的非靜態變數
9、子類的非靜態程式碼塊
10、子類的構造方法
/**
* @ClassName Demo01
* @Description 測試程式初始化順序
* @Author xwd
* @Date 2018/10/23 21:50
*/
public class Demo01 {
public static void main(String[] args) {
B b = new B();
}
}
class A{
static String str1 = "父類A的靜態變數";
String str2 = "父類A的非靜態變數";
static {
System.out.println("執行了父類A的靜態程式碼塊");
}
{
System.out.println("執行了父類A的非靜態程式碼塊");
}
public A(){
System.out.println("執行了父類A的構造方法");
}
}
class B extends A{
static String str1 = "子類B的靜態變數";
String str2 = "子類B的非靜態變數";
static {
System.out.println("執行了子類B的靜態程式碼塊");
}
{
System.out.println("執行了子類B的非靜態程式碼塊");
}
public B(){
System.out.println("執行了子類B的構造方法");
}
}
控制檯輸出:
程式初始化順序三、類的引用
1、主動引用(一定會初始化)
- new一個類的物件。
- 呼叫類的靜態成員(除了final常量)和靜態方法。
- 使用java.lang.reflect包的方法對類進行反射呼叫。
- 當虛擬機器啟動,java Hello,則一定會初始化Hello類。說白了就是先啟動main方法所在的類。
- 當初始化一個類,如果其父類沒有被初始化,則先會初始化他的父類
2、被動引用
- 當訪問一個靜態域時,只有真正宣告這個域的類才會被初始化。例如:通過子類引用父類的靜態變數,不會導致子類初始化。
- 通過陣列定義類引用,不會觸發此類的初始化。
- 引用常量不會觸發此類的初始化(常量在編譯階段就存入呼叫類的常量池中了)。
/**
* @ClassName Demo02
* @Description 測試類的引用
* @Author xwd
* @Date 2018/10/23 21:58
*/
public class Demo02 {
public static void main(String[] args) throws ClassNotFoundException {
//主動引用:new一個類的物件
// People people = new People();
//主動引用:呼叫類的靜態成員(除了final常量)和靜態方法
// People.getAge();
// System.out.println(People.age);
//主動呼叫:使用java.lang.reflect包的方法對類進行反射呼叫
// Class.forName("pri.xiaowd.classloader.People");
//被動引用:當訪問一個靜態域時,只有真正宣告這個域的類才會被初始化。
// System.out.println(WhitePeople.age);
//被動引用:通過陣列定義引用,不會初始化
// People[] people = new People[10];
//被動引用:引用常量不會觸發此類的初始化
System.out.println(People.num);
}
//主動呼叫:先啟動main方法所在的類
// static {
// System.out.println("main方法所在的類在虛擬機器啟動時就載入");
// }
}
class People{
static int age = 3;
static final int num = 20;
static {
System.out.println("People被初始化了!");
}
public People() {
}
public static int getAge() {
return age;
}
public static void setAge(int age) {
People.age = age;
}
}
class WhitePeople extends People{
static {
System.out.println("WhitePeople被初始化了!");
}
}
四、類載入器的原理
1、類快取
標準的Java SE類載入器可以按要求查詢類,一旦某個類被載入到類載入器中,它將維持載入(快取)一段時間。不過,JVM垃圾收集器可以回收這些Class物件。
2、類載入器的分類
類載入器的分類引導類載入器(bootstrap class loader)
(1)它用來載入 Java 的核心庫(JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path路徑下的內容),是用原生程式碼(C語言)來實現的,並不繼承自 java.lang.ClassLoader。
(2)載入擴充套件類和應用程式類載入器。並指定他們的父類載入器。
擴充套件類載入器(extensions class loader)
(1)用來載入 Java 的擴充套件庫(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路徑下的內容) 。Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java類。
(2)由sun.misc.Launcher$ExtClassLoader實現。
應用程式類載入器(application class loader)
(1)它根據 Java 應用的類路徑(classpath,java.class.path 路徑下的內容)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。
(2)由sun.misc.Launcher$AppClassLoader實現。
自定義類載入器
(1)開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類載入器,以滿足一些特殊的需求。
3、java.class.ClassLoader類
(1)作用:
-
java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的位元組程式碼,然後從這些位元組程式碼中定義出一個Java類,即java.lang.Class類的一個例項。
-
ClassLoader還負責載入 Java 應用所需的資源,如影象檔案和配置檔案等。
(2)常用方法: -
getParent() 返回該類載入器的父類載入器。
-
loadClass(String name) 載入名稱為 name的類,返回的結果是java.lang.Class類的例項。
此方法負責載入指定名字的類,首先會從已載入的類中去尋找,如果沒有找到;從parent ClassLoader[ExtClassLoader]中載入;如果沒有載入到,則從Bootstrap ClassLoader中嘗試載入(findBootstrapClassOrNull方法), 如果還是載入失敗,則自己載入。如果還不能載入,則丟擲異常ClassNotFoundException。 -
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 類。
五、類載入器的代理模式
代理模式即是將指定類的載入交給其他的類載入器。常用雙親委託機制。
1、雙親委託機制
某個特定的類載入器接收到類載入的請求時,會將載入任務委託給自己的父類,直到最高階父類引導類載入器(bootstrap class loader),如果父類能夠載入就載入,不能載入則返回到子類進行載入。如果都不能載入則報錯。ClassNotFoundException
雙親委託機制雙親委託機制是為了保證 Java 核心庫的型別安全。這種機制保證不會出現使用者自己能定義java.lang.Object類等的情況。例如,使用者定義了java.lang.String,那麼載入這個類時最高階父類會首先載入,發現核心類中也有這個類,那麼就載入了核心類庫,而自定義的永遠都不會載入。
值得注意是,雙親委託機制是代理模式的一種,但並不是所有的類載入器都採用雙親委託機制。在tomcat伺服器類載入器也使用代理模式,所不同的是它是首先嚐試去載入某個類,如果找不到再代理給父類載入器。這與一般類載入器的順序是相反的。
六、自定義類載入器
自定義類載入器的流程
(1)首先檢查請求的型別是否已經被這個類裝載器裝載到名稱空間中了,如果已經裝載,直接返回;否則轉入步驟2。
(2)委派類載入請求給父類載入器,如果父類載入器能夠完成,則返回父類載入器載入的Class例項;否則轉入步驟3。
(3)呼叫本類載入器的findClass(…)方法,試圖獲取對應的位元組碼,如果獲取的到,則呼叫defineClass(…)匯入型別到方法區;如果獲取不到對應的位元組碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉拋異常,終止載入過程(注意:這裡的異常種類不止一種)。
- 注意:被兩個類載入器載入的同一個類,JVM認為是不相同的類。
import java.io.*;
/**
* @ClassName FileSystemClassLoader
* @Description 自定義檔案類載入器
* @Author xwd
* @Date 2018/10/24 9:23
*/
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;//根目錄
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* @MethodName findClass
* @Descrition 載入類
* @Param [name]
* @return java.lang.Class<?>
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> loadedClass = findLoadedClass(name);//查詢該類是否已經被載入過
if(loadedClass != null){ //該類已經被載入過了,直接返回
return loadedClass;
}else{ //該類還沒有被載入過
ClassLoader classLoader = this.getParent();//委派給父類載入
try {
loadedClass = classLoader.loadClass(name);
} catch (ClassNotFoundException e) {
// e.printStackTrace();
}
if(loadedClass != null){ //父類載入成功,返回
return loadedClass;
}else{
byte[] classData = getClassData(name);
if(classData == null){
throw new ClassNotFoundException();
}else{
loadedClass = defineClass(getName(),classData,0,classData.length);
}
}
}
return loadedClass;
}
/**
* @MethodName getClassData
* @Descrition 根據類名獲得對應的位元組陣列
* @Param [name]
* @return byte[]
*/
private byte[] getClassData(String name) {
//pri.xiaowd.test.A --> D:/myjava/pei/xiaowd/test/A.class
String path = rootDir + "/" + name.replace('.','/') + ".class";
// System.out.println(path);
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
is = new FileInputStream(path);
byte[] bytes = new byte[1024];
int temp = 0;
while((temp = is.read(bytes)) != -1){
baos.write(bytes,0,temp);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();