1. 程式人生 > 實用技巧 >深入淺出JVM(Ⅰ):JVM規範&類從載入、連線、初始化到解除安裝

深入淺出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生成的非正式“虛擬機器組合語言”,格式如下:

[[]...][comment]

是指令操作碼在陣列中的下標,該陣列以位元組形式儲存當前方法java虛擬機器程式碼;也可以是相對於方法起始處的位元組偏移量

是指令的助記碼、是運算元、是行尾的註釋

ASM介紹

概述

ASM是一個Java位元組碼操縱框架,能夠用來動態生成或增強既有類的功能

ASM程式設計模型

Core API

提供基於事件形式程式設計模型。不需要一次性將整個類結構讀取到記憶體,執行更快、佔用記憶體少,但是程式設計方式難度較大

ASM Core API中操縱位元組碼的功能基於ClassVisitor介面,這個介面中的每個方法對應class檔案中每一項

  • ClassReader: 解析class位元組碼
  • ClassAdapter: ClassVisitor實現類,實現變化功能
  • ClassWriter: ClassVisitor實現類,輸出變化後的位元組碼

ASM提供ASMifier工具,可用來生成ASM結構來對比

Tree API

提供基於樹形的程式設計模型。需要一次性將整個類結構讀取到記憶體,佔用更多記憶體但是程式設計方式簡單。

類載入、類載入器,雙親委派模型

類載入

  1. 通過類的全限定名獲取該類的二進位制位元組流
  2. 把二進位制位元組流轉化為方法區的執行時資料結構
  3. 在堆上建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構,並向外提供訪問方法區內資料結構的介面
  • 常見方式:本地檔案、jar等歸檔檔案中載入
  • 動態方式:將java原始檔動態編譯成class
  • 其它方式:網路下載、從專有資料庫中載入等

類載入器

Java虛擬機器自帶載入器包括以下幾種:

  • 啟動類載入器(BootstrapClassLoader)
  • 平臺類載入器(PlatformClassLoader) jdk9, jdk8: 擴充套件類載入器ExtensionClassLoader
  • 應用程式類載入器(AppClassLoader)

使用者自定義載入器,是java.lang.ClassLoader的子類,使用者可以定製類的載入方式,自定義載入器載入順序在所有系統類載入器之後

類載入器的關係

雙親委派模型

JVM中的ClassLoader通常採用雙親委派模型,要求除啟動類載入器外,其餘的類載入器都應該有自己的父載入器。載入器間是組合關係而非繼承。工作過程如下:

  1. 類載入器接收到類載入請求後。首先搜尋它的內建載入器定義的所有“具名模組”
  2. 如果找到了合適的模組定義,將會使用該載入器來載入
  3. 如果class沒有在這些載入器定義的具名模組中找到,那麼將會委託給父載入器,直到啟動類載入器
  4. 如果父載入器反饋不能完成請求,比如在它的搜尋路徑下找不到這個類,那子類載入器自己來載入
  5. 在類路徑下找到的類成為這些載入器的無名模組

雙親委派模型說明:

  1. 雙親委派模型有利於保證Java程式的穩定
  2. 實現雙親委派的程式碼在java.class.ClassLoader的loadClass()方法中,自定義類載入器推薦重寫findClass()方法
  3. 如果有一個類載入器能載入某個類,成為定義類載入器,所有能成功返回該類的Class的類載入器都被稱為初始類載入器
  4. 如果沒有指定父載入器,預設就是啟動類載入器
  5. 每個類載入器都有自己的名稱空間,名稱空間由該載入器及其所有父載入器所載入的類構成,不同的名稱空間可以出現類的全路徑相同的情況
  6. 執行時包由同一個類載入器的類構成,決定兩個類是否屬於同一個執行時包不僅要看全路徑是否一樣,還要看定義類載入器是否相同。只有屬於同一個執行時包的類才能實現相互包可見

自定義類載入器:

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執行環境中去,包含以下幾個步驟:

  1. 驗證:確保被載入類的正確性
    • 類檔案結構驗證
    • 元資料驗證
    • 位元組碼驗證
    • 符號引用驗證
  2. 解析:把常量池中的符號引用換為直接引用

類初始化

為類的靜態變數賦初始值,或者說執行類的構造器方法

  1. 如果類未載入或連線,先進行載入連線
  2. 如果存在父類且父類未初始化,先初始化父類
  3. 如果類中存在初始化語句,依次執行
  4. 如果是介面
    • 初始化類不會先初始化它實現的介面
    • 初始化介面不會初始化父介面
    • 只有程式首次使用介面中的變數或呼叫介面方法時,接口才會初始化
  5. ClassLoader類的loadClass()方法裝載類不會初始化這個類,不是對類的主動使用

類初始化時機

Java程式對類的使用分成: 主動使用和被動使用。JVM必須在每個類或介面“首次主動使用”時才會初始化它們,被動使用的類不會導致類的初始化。

主動使用的情況:

  1. 建立類例項
  2. 訪問類或介面的靜態變數
  3. 呼叫類的靜態方法
  4. 反射某個類
  5. 初始化子類,父類還沒初始化
  6. JVM啟動時執行的主類
  7. 定義了default方法的介面,當介面實現類初始化

類解除安裝

當代表類的Class物件不再被引用,那麼Class物件生命週期就結束了,對應方法區的資料也會被解除安裝

JVM自帶的類載入器裝載的類不會解除安裝,由使用者自定義的類載入器載入的類可以被解除安裝