本文仍然基於JDK8版本,從JDK9模塊化器,類加載器有一些變動。
0 javac編譯
java代碼
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
System.out.println("end");
}
}
javac 編譯,javap -v -p 查看class文件
Classfile /F:/workspace/advanced-java/target/classes/com/lzp/java/jvm/classloader/Math.class
// 第1部分,描述信息:大小、修改時間、md5值等
Last modified 2022年1月8日; size 1006 bytes
MD5 checksum 4cece4543963b23a98cd219a59c1887c
Compiled from "Math.java"
// 第2部分,描述信息:編譯版本
public class com.lzp.java.jvm.classloader.Math
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // com/lzp/java/jvm/classloader/Math
super_class: #11 // java/lang/Object
interfaces: 0, fields: 2, methods: 4, attributes: 1
// 第3部分,常量池信息
Constant pool:
#1 = Methodref #11.#39 // java/lang/Object."<init>":()V
#2 = Class #40 // com/lzp/java/jvm/classloader/Math
#3 = Methodref #2.#39 // com/lzp/java/jvm/classloader/Math."<init>":()V
#4 = Methodref #2.#41 // com/lzp/java/jvm/classloader/Math.compute:()I
#5 = Fieldref #42.#43 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #44 // end
#7 = Methodref #45.#46 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #47 // com/lzp/java/jvm/classloader/User
#9 = Methodref #8.#39 // com/lzp/java/jvm/classloader/User."<init>":()V
#10 = Fieldref #2.#48 // com/lzp/java/jvm/classloader/Math.user:Lcom/lzp/java/jvm/classloader/User;
#11 = Class #49 // java/lang/Object
#12 = Utf8 initData
#13 = Utf8 I
#14 = Utf8 ConstantValue
#15 = Integer 666
#16 = Utf8 user
#17 = Utf8 Lcom/lzp/java/jvm/classloader/User;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/lzp/java/jvm/classloader/Math;
#25 = Utf8 compute
#26 = Utf8 ()I
#27 = Utf8 a
#28 = Utf8 b
#29 = Utf8 c
#30 = Utf8 main
#31 = Utf8 ([Ljava/lang/String;)V
#32 = Utf8 args
#33 = Utf8 [Ljava/lang/String;
#34 = Utf8 math
#35 = Utf8 MethodParameters
#36 = Utf8 <clinit>
#37 = Utf8 SourceFile
#38 = Utf8 Math.java
#39 = NameAndType #18:#19 // "<init>":()V
#40 = Utf8 com/lzp/java/jvm/classloader/Math
#41 = NameAndType #25:#26 // compute:()I
#42 = Class #50 // java/lang/System
#43 = NameAndType #51:#52 // out:Ljava/io/PrintStream;
#44 = Utf8 end
#45 = Class #53 // java/io/PrintStream
#46 = NameAndType #54:#55 // println:(Ljava/lang/String;)V
#47 = Utf8 com/lzp/java/jvm/classloader/User
#48 = NameAndType #16:#17 // user:Lcom/lzp/java/jvm/classloader/User;
#49 = Utf8 java/lang/Object
#50 = Utf8 java/lang/System
#51 = Utf8 out
#52 = Utf8 Ljava/io/PrintStream;
#53 = Utf8 java/io/PrintStream
#54 = Utf8 println
#55 = Utf8 (Ljava/lang/String;)V
{
// 第四部分,變量信息
public static final int initData;
descriptor: I
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 666
public static com.lzp.java.jvm.classloader.User user;
descriptor: Lcom/lzp/java/jvm/classloader/User;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
public com.lzp.java.jvm.classloader.Math();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lzp/java/jvm/classloader/Math;
// 第五部分,方法信息
public int compute();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 9: 0
line 10: 2
line 11: 4
line 12: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/lzp/java/jvm/classloader/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/lzp/java/jvm/classloader/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String end
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
line 19: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 args [Ljava/lang/String;
8 14 1 math Lcom/lzp/java/jvm/classloader/Math;
MethodParameters:
Name Flags
args
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #8 // class com/lzp/java/jvm/classloader/User
3: dup
4: invokespecial #9 // Method com/lzp/java/jvm/classloader/User."<init>":()V
7: putstatic #10 // Field user:Lcom/lzp/java/jvm/classloader/User;
10: return
LineNumberTable:
line 6: 0
}
方法中的#1/2,可以到ConstantPool找到對應符號。
參考字節碼指令表:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html。
1 類加載過程
j經典的類加載過程如下圖,包括加載、鏈接、初始化三部分。
1.1 加載class文件
字節碼文件位於磁盤,當使用到某個類(例如,調用main()方法,new新對象),在磁盤中查找並通過IO讀取文件的二進制流,轉爲方法區數據結構,並存放到方法區,在Java堆中產生 java.lang.Class
對象。Class對象是可以方法區的訪問入口,用於Java反射機制,獲取類的各種信息。
1.2 鏈接過程
驗證:驗證class文件是不是符合規範
-
文件格式的驗證。驗證是否以0XCAFEBABE開頭,版本號是否合理
-
元數據驗證。是否有父類,是否繼承了final類(final類不能被繼承),非抽象類實現了所有抽象方法。
-
字節碼驗證。(略)
-
符號引用驗證。常量池中描述類是否存在,訪問的方法或字段是否存在且有足夠的權限。
-Xverify:none // 取消驗證
準備:爲類的靜態變量分配內存,初始化爲系統的初始值
final static修飾的變量:直接賦值爲用戶定義的值,比如 private final static int value=123,直接賦值123。
private static int value=123
,該階段的值依然是0。
解析:符號引用轉換成直接引用(靜態鏈接)
Java代碼中每個方法、方法參數都是符號,類加載放入方法區的常量池Constant pool中。
符號引用:應該可以理解成常量池中的這些字面量。【可能沒理解對】
直接引用:符號對應代碼被加載到JVM內存中的位置(指針、句柄)。
靜態鏈接過程在類加載時完成,主要轉換一些靜態方法。動態鏈接是在程序運行期間完成的將符號引用替換爲直接引用。
1.3 初始化(類初始化clinit-->初始化init)
執行< clinit>
方法, clinit
方法由編譯器自動收集類裏面的所有靜態變量的賦值動作及靜態語句塊合併而成,也叫類構造器方法。
-
初始化的順序和源文件中的順序一致
-
子類的
< clinit>
被調用前,會先調用父類的< clinit>
-
JVM會保證
clinit
方法的線程安全性
初始化時,如果實例化一個新對象,會調用<init>
方法對實例變量進行初始化,並執行對應的構造方法內的代碼。
類加載過程是懶加載的,用到纔會加載。
初始化示例
public class JVMTest2 {
static {
System.out.println("JVMTest2靜態塊");
}
{
System.out.println("JVMTest2構造塊");
}
public JVMTest2() {
System.out.println("JVMTest2構造方法");
}
public static void main(String[] args) {
System.out.println("main方法");
new Sub();
}
}
class Super {
static {
System.out.println("Super靜態代碼塊");
}
public Super() {
System.out.println("Super構造方法");
}
{
System.out.println("Super普通代碼塊");
}
}
class Sub extends Super {
static {
System.out.println("Sub靜態代碼塊");
}
public Sub() {
System.out.println("Sub構造方法");
}
{
System.out.println("Sub普通代碼塊");
}
}
JVMTest2靜態塊
main方法
Super靜態代碼塊
Sub靜態代碼塊
Super普通代碼塊
Super構造方法
Sub普通代碼塊
Sub構造方法
執行main方法,並不需要創建JVMTest2實例。
對於普通代碼塊,以前認爲是和clinit
一樣順序加載。其實是不一樣的,普通代碼塊編譯時對賦值語句和其他語句分別做了優化,如下賦值語句優化爲int i = 1;
打印語句優化爲構造方法的第一句。
源代碼
public class JVMTest1 {
int i;
{
i = 1;
System.out.println("JVMTest1構造塊");
}
public JVMTest1(){
System.out.println("JVMTest1構造方法");
}
}
反編譯後的代碼
public class JVMTest1 {
int i = 1;
public JVMTest1() {
System.out.println("JVMTest1構造塊");
System.out.println("JVMTest1構造方法");
}
}
2 類加載器
查看當前JDK類加載器
public class PrintJDKClassLoader {
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
ClassLoader parentParent = parent.getParent();
System.out.println(parentParent);
}
}
// JDK8
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@28a418fc
null
// JDK11
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1324409e
null
2.1 類加載器(JDK8)
類加載器初始化過程:Java通過調用jvm.dll文件創建JVM,創建一個引導類加載器(由C++實現),通過JVM啓動器(sun.misc.Launcher)加載擴展類加載器和應用類加載器。
-
啓動類加載器:負責加載lib目錄下的核心類庫。作爲JVM的一部分,由C++實現。
-
擴展類/平臺類加載器:負責加載lib目錄下的ext擴展目錄中的JAR 類包。
-
應用程序類加載器:負責加載用戶類路徑ClassPath路徑下的類包,主要就是加載用戶自己寫的類。
-
自定義類加載器:負責加載用戶自定義路徑下的類包。
JVM默認使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實例加載我們的應用程序。
// Launcher構造方法
public Launcher() {
Launcher.ExtClassLoader var1;
// 構造擴展類加載器,設置類加載器parent屬性設爲null。
var1 = Launcher.ExtClassLoader.getExtClassLoader();
// 構造應用類加載器,設置類加載器parent屬性爲擴展類加載器。
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
// 權限校驗代碼..
}
}
2.2 雙親委派模型
類加載器採用三層、雙親委派模型,類加載器的父子關係不是繼承關係,而是組合關係。除了啓動類加載器外,其他類加載器都是繼承自ClassLoader類。
工作過程:類加載器收到類加載請求,首先判斷類是否已經加載,如果未被加載,嘗試將請求向上委派給父類加載器加載。當父類加載器無法完成加載任務,再由子類加載器嘗試加載。
// ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 檢查類是否已被加載
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 非啓動類加載器
c = parent.loadClass(name, false);
} else {
// 啓動類加載器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父類加載器無法加載指定類
}
if (c == null) {
// 調用當前類加載器的findClass方法進行類加載
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
爲什麼使用雙親委派模型,感覺走了彎路?
雙親委派模型下,類加載請求總會被委派給最上層的啓動類加載器。對於未加載的類來說,需要從底層走到頂層;如果用戶定義的類已經被加載過,則不需要委派過程。
使用雙親委派機制有下面幾個好處:
- 沙箱安全機制,防止核心類庫代碼被篡改。
- 避免類重複加載,父類加載器加載過,子類加載器不需要再次加載。
全盤負責委託機制
全盤負責 :即是當一個classloader加載一個Class的時候,這個Class所依賴的和引用的其它Class 通常 也由這個classloader負責載入。 委託機制 :先讓parent(父)類加載器 尋找,只有在parent找不到的時候才從自己的類路徑中去尋找。
參考Launcher構造方法
Thread.currentThread().setContextClassLoader(this.loader);
自定義類加載器
自定義類加載器操作主要是繼承ClassLoader類,重寫上面源碼中的findClass(name)方法。
public class CustomClassLoaderTest {
static class CustomClassLoader extends ClassLoader {
private String classFilePath;
public CustomClassLoader(String classFilePath) {
this.classFilePath = classFilePath;
}
// 載入class數據流
private byte[] loadClassFile(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classFilePath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException{
try {
byte[] data = loadClassFile(name);
// 加載--鏈接--初始化等邏輯
return defineClass(name,data,0,data.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
public static void main(String[] args) throws Exception {
CustomClassLoader classLoader = new CustomClassLoader("F:");
Class<?> clazz = classLoader.loadClass("com.lzp.java.jvm.classloader.JVMTest");
Object instance = clazz.newInstance();
Method method = clazz.getDeclaredMethod("add", null);
System.out.println(method.invoke(instance));
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
自定義類加載器的父加載器是應用類加載器。CustomClassLoader是使用AppClassLoader運行的,自然而然是父類加載器。
打破雙親委派機制
在一些場景下,打破雙親委派是必要的。例如Tomcat中可能有多個應用,引用了不同的Spring版本。打破雙親委派,可以實現應用隔離。
JVM使用loadClass方法實現雙親委派機制。重寫loadClass方法,便可以打破雙親委派機制。
直接刪除雙親委派代碼是不可行的,Java代碼繼承自Object,總會需要雙親委派來加載核心代碼。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
// 非自定義的類還是走雙親委派加載
if (!name.equals("com.lzp.java.jvm.classloader.JVMTest")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
注:JDK自帶的核心庫代碼,是不允許自行配置修改的。例如,不可以將Object.class拷出來執行。沙箱隔離。