深入淺出JVM(Ⅰ):JVM規範&類從載入、連線、初始化到解除安裝
JVM指令集
JVM虛擬機器規範詳情參見官網
Class位元組碼
ClassFile結構
ClassFile { u4 magic; // 魔數值,確認class檔案,值固定 u2 minor_version; // 副版本號 u2 major_version; // 主版本號 u2 constant_pool_count; // 常量池計數器 cp_info constant_pool[constant_pool_count-1]; // 常量池 u2 access_flags; // 訪問標誌 u2 this_class; // 類索引 u2 super_class; // 父類索引 u2 interfaces_count; // 介面計數器 u2 interfaces[interfaces_count]; // 介面表 u2 fields_count; // 欄位計數器 field_info fields[fields_count]; // 欄位表 u2 methods_count; // 方法計數器 method_info methods[methods_count]; // 方法表 u2 attributes_count; // 屬性計數器 attribute_info attributes[attributes_count]; // 屬性表 }
javap反編譯class檔案
javap
-help --help -? 輸出此用法訊息 -version 版本資訊,其實是當前javap所在jdk的版本資訊,不是class在哪個jdk下生成的。 -v -verbose 輸出附加資訊(包括行號、本地變量表,反彙編等詳細資訊) -l 輸出行號和本地變量表 -public 僅顯示公共類和成員 -protected 顯示受保護的/公共類和成員 -package 顯示程式包/受保護的/公共類 和成員 (預設) -p -private 顯示所有類和成員 -c 對程式碼進行反彙編 -s 輸出內部型別簽名 -sysinfo 顯示正在處理的類的系統資訊 (路徑, 大小, 日期, MD5 雜湊) -constants 顯示靜態最終常量 -classpath <path> 指定查詢使用者類檔案的位置 -bootclasspath <path> 覆蓋引導類檔案的位置
javap生成的非正式“虛擬機器組合語言”,格式如下:
ASM介紹
概述
ASM是一個Java位元組碼操縱框架,能夠用來動態生成或增強既有類的功能
ASM程式設計模型
Core API
提供基於事件形式程式設計模型。不需要一次性將整個類結構讀取到記憶體,執行更快、佔用記憶體少,但是程式設計方式難度較大
ASM Core API中操縱位元組碼的功能基於ClassVisitor介面,這個介面中的每個方法對應class檔案中每一項
- ClassReader: 解析class位元組碼
- ClassAdapter: ClassVisitor實現類,實現變化功能
- ClassWriter: ClassVisitor實現類,輸出變化後的位元組碼
ASM提供ASMifier工具,可用來生成ASM結構來對比
Tree API
提供基於樹形的程式設計模型。需要一次性將整個類結構讀取到記憶體,佔用更多記憶體但是程式設計方式簡單。
類載入、類載入器,雙親委派模型
類載入
- 通過類的全限定名獲取該類的二進位制位元組流
- 把二進位制位元組流轉化為方法區的執行時資料結構
- 在堆上建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構,並向外提供訪問方法區內資料結構的介面
- 常見方式:本地檔案、jar等歸檔檔案中載入
- 動態方式:將java原始檔動態編譯成class
- 其它方式:網路下載、從專有資料庫中載入等
類載入器
Java虛擬機器自帶載入器包括以下幾種:
- 啟動類載入器(BootstrapClassLoader)
- 平臺類載入器(PlatformClassLoader) jdk9, jdk8: 擴充套件類載入器ExtensionClassLoader
- 應用程式類載入器(AppClassLoader)
使用者自定義載入器,是java.lang.ClassLoader的子類,使用者可以定製類的載入方式,自定義載入器載入順序在所有系統類載入器之後
類載入器的關係
雙親委派模型
JVM中的ClassLoader通常採用雙親委派模型,要求除啟動類載入器外,其餘的類載入器都應該有自己的父載入器。載入器間是組合關係而非繼承。工作過程如下:
- 類載入器接收到類載入請求後。首先搜尋它的內建載入器定義的所有“具名模組”
- 如果找到了合適的模組定義,將會使用該載入器來載入
- 如果class沒有在這些載入器定義的具名模組中找到,那麼將會委託給父載入器,直到啟動類載入器
- 如果父載入器反饋不能完成請求,比如在它的搜尋路徑下找不到這個類,那子類載入器自己來載入
- 在類路徑下找到的類成為這些載入器的無名模組
雙親委派模型說明:
- 雙親委派模型有利於保證Java程式的穩定
- 實現雙親委派的程式碼在java.class.ClassLoader的loadClass()方法中,自定義類載入器推薦重寫findClass()方法
- 如果有一個類載入器能載入某個類,成為定義類載入器,所有能成功返回該類的Class的類載入器都被稱為初始類載入器
- 如果沒有指定父載入器,預設就是啟動類載入器
- 每個類載入器都有自己的名稱空間,名稱空間由該載入器及其所有父載入器所載入的類構成,不同的名稱空間可以出現類的全路徑相同的情況
- 執行時包由同一個類載入器的類構成,決定兩個類是否屬於同一個執行時包不僅要看全路徑是否一樣,還要看定義類載入器是否相同。只有屬於同一個執行時包的類才能實現相互包可見
自定義類載入器:
public class MyClassLoader extends ClassLoader {
private String loaderName;
public MyClassLoader(String loaderName) {
this.loaderName = loaderName;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}
private byte[] loadClassData(String name) {
byte[] data = null;
name = name.replace(".", "/");
try (ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStream in = new FileInputStream(new File(
"target/" + name + ".class"))){
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
data = out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return data;
}
}
public class MyClass {
public MyClass() {
}
}
public class ClassCloaderMain {
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classLoader = new MyClassLoader("myClassLoader1");
Class cls = classLoader.loadClass("classloader.MyClass");
System.out.println("cls class loader == " + cls.getClassLoader());
System.out.println("cls parent class loader == " + cls.getClassLoader().getParent());
}
}
/*
控制檯列印:
cls class loader == classloader.MyClassLoader@3caeaf62
cls parent class loader == sun.misc.Launcher$AppClassLoader@18b4aac2
*/
破壞雙親委派模型:
-
雙親委派模型問題: 父載入器無法向下識別子載入器載入的資源
為了解決這個問題,引入執行緒上下文類載入器,可以通過Thread的setContextClassLoader()進行設定,例如資料庫連線驅動載入
-
另一種典型情況是實現熱替換,比如OSGI的模組熱部署,它的類載入器不再是嚴格按照雙親委派模型,很多在平級的類載入器中執行
類連線
將已經讀入記憶體的類二進位制資料合併到JVM執行環境中去,包含以下幾個步驟:
- 驗證:確保被載入類的正確性
- 類檔案結構驗證
- 元資料驗證
- 位元組碼驗證
- 符號引用驗證
- 解析:把常量池中的符號引用換為直接引用
類初始化
為類的靜態變數賦初始值,或者說執行類的構造器
- 如果類未載入或連線,先進行載入連線
- 如果存在父類且父類未初始化,先初始化父類
- 如果類中存在初始化語句,依次執行
- 如果是介面
- 初始化類不會先初始化它實現的介面
- 初始化介面不會初始化父介面
- 只有程式首次使用介面中的變數或呼叫介面方法時,接口才會初始化
- ClassLoader類的loadClass()方法裝載類不會初始化這個類,不是對類的主動使用
類初始化時機
Java程式對類的使用分成: 主動使用和被動使用。JVM必須在每個類或介面“首次主動使用”時才會初始化它們,被動使用的類不會導致類的初始化。
主動使用的情況:
- 建立類例項
- 訪問類或介面的靜態變數
- 呼叫類的靜態方法
- 反射某個類
- 初始化子類,父類還沒初始化
- JVM啟動時執行的主類
- 定義了default方法的介面,當介面實現類初始化
類解除安裝
當代表類的Class物件不再被引用,那麼Class物件生命週期就結束了,對應方法區的資料也會被解除安裝
JVM自帶的類載入器裝載的類不會解除安裝,由使用者自定義的類載入器載入的類可以被解除安裝