超易懂Java 類加載機制詳解,這波示例教程真的愛瞭

引子

Java 虛擬機的啟動是通過引導類加載器 ( Bootstrap Class Loader ) 創建一個初始類 (Initial Class ) 來完成,這個類是由虛擬機的具體實現指定。緊接著,Java虛擬機鏈接這個初始類,初始化並調用它的 public void main(String[])方法。之後的整個執行過程都是由對此方法的調用開始。執行 main 方法中的 Java 虛擬機指令可能會導致 Java 虛擬機鏈接另外的一些類或接口,也可能會調用另外的方法。

可能在某種 Java 虛擬機的實現上,初始類會作為命令行參數被提供給虛擬機。當然,虛擬機實現也可以利用一個初始類讓類加載器依次加載整個應用。初始類當然也可以選擇組合上述的方式來工作。

—— 以上內容摘自《Java 虛擬機規范》(Java SE 7 版)

在講類的加載機制前,先來看一道題目:

public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println("爸爸的歲數:" + Son.factor); //入口1
// new Son(); //入口 2
}
}

class Grandpa {
static {
System.out.println("爺爺在靜態代碼塊");
}
public Grandpa() {
System.out.println("我是爺爺~");
}
}

class Father extends Grandpa {
static {
System.out.println("爸爸在靜態代碼塊");
}
public static int factor = 25;

public Father() {
System.out.println("我是爸爸~");
}
}

class Son extends Father {
static {
System.out.println("兒子在靜態代碼塊");
}

public Son() {
System.out.println("我是兒子~");
}
}

上面的代碼中分瞭 入口1 和 入口2 , 兩者不同時存在,入口不一樣,最後輸出的結果也是不一樣的。小夥伴可以思考下這兩個入口對於類的初始化有啥不一樣。下面是具體結果:

入口1的結果:

爺爺在靜態代碼塊
爸爸在靜態代碼塊
爸爸的歲數:25

入口2 的結果:

爺爺在靜態代碼塊
爸爸在靜態代碼塊
兒子在靜態代碼塊
我是爺爺~
我是爸爸~
我是兒子~

如果以前沒有遇到這種問題,現在要你解答肯定是很難的。該題考察的就是你對 Java 類加載機制的理解。如果你對 Java 加載機制不理解,那麼你是無法解答這道題目的。

對比上面兩個結果,可以發現,入口1 都是靜態代碼的初始化,入口2 既涉及到靜態代碼的初始化,也涉及到類的初始化。到此大傢肯定就知道對於靜態代碼和非靜態代碼的初始化邏輯是有區別的。

這篇文章,將對 Java 類加載機制的進行講解,讓你以後遇到類似問題不在犯難。

類的加載過程

當 Java 虛擬機將 Java 源碼編譯為字節碼之後,虛擬機便可以將字節碼讀取進內存,從而進行解析、運行等整個過程,這個過程我們叫:Java 虛擬機的類加載機制。JVM 虛擬機執行 class 字節碼的過程可以分為七個階段: 加載、驗證、準備、解析、初始化、使用、卸載 。 其中加載、檢驗、準備、初始化和卸載這個五個階段的順序是固定的,而解析則未必。為瞭支持動態綁定,解析這個過程可以發生在初始化階段之後。

超易懂Java 類加載機制詳解,這波示例教程真的愛瞭

1. 加載

什麼情況下需要開始 類加載 的第一個階段:加載。 JAVA虛擬機規范並沒有進行強制約束,交給虛擬機的具體實現自由把握。

加載階段是“類加載”過程中的一個階段,這個階段通常也被稱作“裝載”,在加載階段,虛擬機主要完成以下3件事情:

  1. 通過 “類全名” 來獲取定義此類的二進制字節流
  2. 將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
  3. 在 java 堆中生成一個代表這個類的 java.lang.Class 對象,作為方法區這些數據的訪問入口(所以我們能夠通過低調用類.getClass() )

註意這裡字節流不一定非得要從一個 Class 文件獲取,這裡既可以從 ZIP 包中讀取(比如從 jar 包和 war 包中讀取),也可以在運行時計算生成(動態代理),也可以由其它文件生成(比如將 JSP 文件轉換成對應的 Class 類)。加載的信息存儲在 JVM 的方法區。

對於數組類來說,它並沒有對應的字節流,而是由 Java 虛擬機直接生成的。對於其它的類來說,Java 虛擬機則需要借助類加載器來完成查找字節流的過程。

如果上面那麼多記不住: 請一定記住這句: 加載階段也就是查找獲取類的二進制數據(磁盤或者網絡)動作,將類的數據(Class 的信息:類的定義或者結構)放入方法區 (內存) 。

一圖說明:

超易懂Java 類加載機制詳解,這波示例教程真的愛瞭

2. 驗證

驗證的主要作用就是確保被加載的類的正確性。也是連接階段的第一步。說白瞭也就是我們加載好的 .class 文件不能對我們的虛擬機有危害,所以先檢測驗證一下。他主要是完成四個階段的驗證:

  1. 文件格式的驗證 :驗證 .class 文件字節流是否符合 class 文件的格式的規范,並且能夠被當前版本的虛擬機處理。這裡面主要對 魔數、主版本號、常量池 等等的校驗(魔數、主版本號都是 .class 文件裡面包含的數據信息、在這裡可以不用理解)。
  2. 元數據驗證 :主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合 java 語言規范的要求,比如說驗證這個類是不是有父類,類中的字段方法是不是和父類沖突等等。
  3. 字節碼驗證 :這是整個驗證過程最復雜的階段,主要是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。在元數據驗證階段對數據類型做出驗證後,這個階段主要對類的方法做出分析,保證類的方法在運行時不會做出威海虛擬機安全的事。
  4. 符號引用驗證 :它是驗證的最後一個階段,發生在虛擬機將符號引用轉化為直接引用的時候。主要是對類自身以外的信息進行校驗。目的是確保解析動作能夠完成。

對整個類加載機制而言,驗證階段是一個很重要但是非必需的階段,如果我們的代碼能夠確保沒有問題,那麼我們就沒有必要去驗證,畢竟驗證需要花費一定的的時間。當然我們可以使用 -Xverfity:none 來關閉大部分的驗證。

3. 準備(重點)

當完成字節碼文件的校驗之後,JVM 便會開始為類變量分配內存並初始化。這裡需要註意兩個關鍵點,即內存分配的對象以及初始化的類型。

  • 內存分配的對象。 Java 中的變量有 「類變量」 和 「類成員變量」 兩種類型, 「類變量」指的是被 static 修飾的變量,而其他所有類型的變量都屬於 「類成員變量」 。在準備階段,JVM 隻會為 「類變量」 分配內存,而不會為 「類成員變量」 分配內存。 「類成員變量」 的內存分配需要等到初始化階段才開始。

例如下面的代碼在準備階段,隻會為 factor 屬性分配內存,而不會為 website 屬性分配內存。

public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";
  • 初始化的類型。 在準備階段, JVM 會為類變量分配內存,並為其初始化 。但是這裡的初始化指的是為 變量賦予 Java 語言中該數據類型的零值 ,而不是用戶代碼裡初始化的值。

例如下面的代碼在準備階段之後,sector 的值將是 0,而不是 3。

public static int sector = 3;

但如果一個變量是常量 (被 static final 修飾)的話,那麼在準備階段,屬性便會被賦予用戶希望的值 。例如下面的代碼在準備階段之後,number 的值將是 3,而不是 0。

public static final int number = 3;

之所以 static final 會直接被復制,而 static 變量會被賦予零值。其實我們稍微思考一下就能想明白瞭。

兩個語句的區別是一個有 final 關鍵字修飾,另外一個沒有。而 final 關鍵字在 Java 中代表不可改變的意思,意思就是說 number 的值一旦賦值就不會在改變瞭。既然一旦賦值就不會再改變,那麼就必須一開始就給其賦予用戶想要的值,因此被 final 修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final 修飾的類變量,其可能在初始化階段或者運行階段發生變化,所以就沒有必要在準備階段對它賦予用戶想要的值。

4. 解析

解析階段是虛擬機常量池內的 符號引用 替換為 直接引用 的過程。

符號引用:符號引用是一組符號來描述所引用的目標對象,符號可以是任何形式的字面量,隻要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標對象並不一定已經加載到內存中。Java 虛擬機明確在 Class 文件格式中定義的符號引用的字面量形式。

直接引用:直接引用可以是直接指向目標對象的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機內存佈局實現相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同,如果有瞭直接引用,那引用的目標必定已經在內存中存在。

在解析的階段,解析動作主要針對7類符號引用進行,它們的名稱以及對於常量池中的常量類型和解析報錯信息如下:

| 解析動作 | 符號引用 | 解析可能的報錯 | | ———- | ——————————- | ———————————————————–

| | 類或接口 | CONSTANTClassInfo | java.land.IllegalAccessError

| | 字段 | CONSTANTFieldrefInfo | java.land.IllegalAccessError 或 java.land.NoSuchFieldError

| | 類方法 | CONSTANTMethodefInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError

| | 接口方法 | CONSTANTInterfaceMethoderInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError

| | 方法類型 | CONSTANTMethodTypeInfo |

| | 方法句柄 | CONSTANTMethodhandlerInfo |

| | 調用限定符 | CONSTANTInvokeDynamicInfo |

解析的整個階段在虛擬機中還是比較復雜的,遠比上面介紹的復雜的多,但是很多特別細節的東西我們 可以暫時先忽略,先有個大概的認識和瞭解之後有時間在慢慢深入瞭。

5. 初始化(重點)

類初始階段是類加載過程的最後一步,在上面提到的類加載過程中,除瞭加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘的動作全部由虛擬機主導和控制。初始化階段,是真正開始執行類中定義的 Java 程序代碼(或者說是字節碼)。

在準備階段,變量已經賦值過一次系統要求的初始值(零值),而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源。(或者從另一個角度表達:初始化階段是執行類構造器 () 方法的過程。)

在這個階段,JVM 會根據語句執行順序對類對象進行初始化,一般來說當 JVM 遇到下面 5 種情況的時候會觸發初始化:

  1. 遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的 Java 代碼場景是:使用new 關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被 final 修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2. 使用 java.lang.reflect 包的方法對類進行 反射調用 的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要 先觸發其父類的初始化
  4. 當虛擬機啟動時,用戶需要指定一個要 執行的主類(包含 main() 方法的那個類) ,虛擬機會先初始化這個主類。
  5. 當使用 JDK1.7 動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。

看到上面幾個條件你可能會暈瞭,但是不要緊,不需要背,知道一下就好,後面用到的時候回到找一下就可以瞭。

註意這裡的初始化,並不是說創造的類的實例,而是執行瞭類構造器,簡單來說就是隻對靜態變量,靜態代碼塊進行初始化。對於構造函數隻有在創建實例的時候才會執行。

6. 使用

當 JVM 完成初始化階段之後,JVM 便開始從入口方法開始執行用戶的程序代碼。這個階段也隻是瞭解一下就可以。

7. 卸載

當用戶程序代碼執行完畢後,JVM 便開始銷毀創建的 Class 對象,最後負責運行的 JVM 也退出內存。這個階段也隻是瞭解一下就可以。

8. 引子題目解答

還記得前面的題目嘛,下面開始分析:

入口1

也許會有人問為什麼沒有輸出「兒子在靜態代碼塊」這個字符串?

這是因為對於靜態字段,隻有直接定義這個字段的類才會被初始化(執行靜態代碼塊)。因此通過其子類來引用父類中定義的靜態字段,隻會觸發父類的初始化而不會觸發子類的初始化。

對面上面的這個例子,我們可以從入口開始分析一路分析下去:

  • 首先程序到 main 方法這裡,使用標準化輸出 Son 類中的 factor 類成員變量,但是 Son 類中並沒有定義這個類成員變量。於是往父類去找,我們在 Father 類中找到瞭對應的類成員變量,於是觸發瞭 Father 的初始化。
  • 但根據我們上面說到的初始化的 5 種情況中的第 3 種(當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化)。我們需要先初始化 Father 類的父類,也就是先初始化 Grandpa 類再初始化 Father 類。於是我們先初始化 Grandpa 類輸出:「爺爺在靜態代碼塊」,再初始化 Father 類輸出:「爸爸在靜態代碼塊」。
  • 最後,所有父類都初始化完成之後,Son 類才能調用父類的靜態變量,從而輸出:「爸爸的歲數:25」。

入口2

這裡采用 new 進行初始化,所以先進行父類得初始化。先是執行靜態變量初始化。子類創建對象的同時會先創造父類的對象,因此必須先調用父類的構造方法。

變動

這裡我做瞭一些改變:

public class ClassLoaderTest {
public static void main(String[] args) {
// System.out.println("爸爸的歲數:" + Son.factor); //入口1
new Son(3); //入口 2
}

}

class Grandpa {
int s = 3;

public Grandpa(int s) {
System.out.println("我是爺爺~" );
}
static {
System.out.println("爺爺在靜態代碼塊");
}
}

class Father extends Grandpa {
static {
System.out.println("爸爸在靜態代碼塊");
}
public static int factor = 25;

public Father(int s) {
//super(s);
System.out.println("我是爸爸~");
}
}

class Son extends Father {
static {
System.out.println("兒子在靜態代碼塊");
}

public Son(int s ) {
super(s);
System.out.println("我是兒子~");
}
}

這裡的變動是,父類子類都隻有一個有參構造函數,在初始化子類得時候,不顯示的調用父類的構造函數,運行結果如下:

爺爺在靜態代碼塊 Exception in thread "main"
爸爸在靜態代碼塊
兒子在靜態代碼塊
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Implicit super constructor Grandpa() is undefined. Must explicitly invoke another constructor

at Father.(ClassLoaderTest.java:27)
at Son.(ClassLoaderTest.java:39)
at ClassLoaderTest.main(ClassLoaderTest.java:5)

簡單來說,如果子類構造函數不顯示調用父類的構造函數,這時候在初始化子類得時候,就會去父類尋找無參構造函數,如果父類隻定義瞭有參構造函數,沒有無參構造函數,就會報錯。因此一般來說最好是顯示調用,又或者多定義幾種不同的構造函數,方便在不同場景下調用。

類加載器

把類加載階段的 “通過一個類的全限定名來獲取描述此類的二進制字節流” 這個動作交給虛擬機之外的類加載器來完成。這樣的好處在於,我們可以自行實現類加載器來加載其他格式的類,隻要是二進制字節流就行,這就大大增強瞭加載器靈活性。

系統自帶的類加載器分為三種:

  1. 啟動類加載器 。其它的類加載器都是 java.lang.ClassLoader 的子類,啟動類加載器是由 C++ 實現的,沒有對應的 Java 對象,因此在 Java 中隻能用 null 代替。啟動類加載器加載最為基礎,最為重要的類,如 JRE 的 lib 目錄下 jar 包中的類;擴展類加載器的父類是啟動類加載器,它負責加載相對次要,但又通用的類,如 JRE 的 lib/ext 目錄下jar包中的類
  2. 擴展類加載器 。Java核心類庫提供,負責加載java的擴展庫(加載 JAVA_HOME/jre/ext/*.jar 中的類),開發者可以直接使用擴展類加載器。
  3. 應用程序類加載器 。Java核心類庫提供。應用類加載器的父類加載器則是擴展類加載器,它負責加載應用程序路徑下的類。開發者可以直接使用這個類加載器,若應用程序中沒有定義過自己的類加載器,java 應用的類都是由它來完成加載的。

具體關系如下:

超易懂Java 類加載機制詳解,這波示例教程真的愛瞭

雙親委派機制工作過程:

如果一個類加載器收到瞭類加載器的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父加載器去完成。每個層次的類加載器都是如此,因此所有的加載請求最終都會傳送到 Bootstrap 類加載器(啟動類加載器)中,隻有父類加載反饋自己無法加載這個請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

雙親委派模型的優點:java類隨著它的加載器一起具備瞭一種帶有優先級的層次關系.

例如類 java.lang.Object 它存放在 rt.jart 之中,無論哪一個類加載器都要加載這個類.最終都是雙親委派模型最頂端的 Bootstrap 類加載器去加載.因此Object類在程序的各種類加載器環境中都是同一個類.相反.如果沒有使用雙親委派模型.由各個類加載器自行去加載的話.如果用戶編寫瞭一個稱為 “java.lang.Object” 的類,並存放在程序的 ClassPath 中。那系統中將會出現多個不同的Object類,java類型體系中最基礎的行為也就無法保證,應用程序也將會一片混亂。

這裡也可以用代碼驗證下:

public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
[email protected]
[email protected]
null

跟前面的描述是一致的。啟動類加載器是由 C++ 實現的,沒有對應的 Java 對象,因此在 Java 中隻能用 null 代替。

自定義類加載器

1、為什麼要自定義ClassLoader

因為系統的 ClassLoader 隻會加載指定目錄下的 class 文件,如果你想加載自己的 class 文件,那麼就可以自定義一個 ClassLoader.

而且我們可以根據自己的需求,對 class 文件進行加密和解密。

2. 如何自定義ClassLoader

新建一個類繼承自 java.lang.ClassLoader 重寫它的 findClass 方法。將 class 字節碼數組轉換為 Class 類的實例。調用 loadClass 方法加載即可

代碼實戰:

先是定義一個自定義類加載器

package com.hello.test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
// 指定路徑
private String path ;

public MyClassLoader(String classPath){
path=classPath;
}

/**
* 重寫findClass方法
* @param name 是我們這個類的全路徑
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class findClass(String name) throws ClassNotFoundException {
Class log = null;
// 獲取該class文件字節碼數組
byte[] classData = getData();

if (classData != null) {
// 將class的字節碼數組轉換成Class類的實例
log = defineClass(name, classData, 0, classData.length);
}
return log;
}

/**
* 將class文件轉化為字節碼數組
* @return
*/
private byte[] getData() {

File file = new File(path);
if (file.exists()){
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();

byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {

e.printStackTrace();
}
}
return out.toByteArray();
}else{
return null;
}
}
}

可以再 getData 裡面做很多事情 ,比如加密解密之類的 都是可以的。

接著創建一個試驗 class :

package com.hello.test;

public class Log {
public static void main(String[] args) {
System.out.println("load Log class successfully from log " );
}
}

執行命令行 javac Log.java 生成我們的 Log.class 文件:

超易懂Java 類加載機制詳解,這波示例教程真的愛瞭

最後就是進行加載:

package com.hello.test;

import java.lang.reflect.Method;

public class ClassLoaderTest {
public static void main(String[] args) {
// 這個類class的路徑,自己復制自己電腦的路徑
String classPath = "/Users/yourname/Documents/workspace-sts-3.9.6.RELEASE/HelloWorld/src/Log.class";

MyClassLoader myClassLoader = new MyClassLoader(classPath);
// 類的全稱,對應包名
String packageNamePath = "com.hello.test.Log";
try {
// 加載Log這個class文件
Class Log = myClassLoader.loadClass(packageNamePath);
System.out.println("類加載器是:" + Log.getClassLoader());
// 利用反射獲取main方法
Method method = Log.getDeclaredMethod("main", String[].class);
Object object = Log.newInstance();
String[] arg = {"ad"};
method.invoke(object, (Object) arg);

} catch (Exception e) {
e.printStackTrace();
}
}

}

輸出結果如下:

超易懂Java 類加載機制詳解,這波示例教程真的愛瞭

可以看到是委托父類進行加載的。 到此,關於類加載器的內容就說完瞭。

鞏固練習

最後我們再來看一道升級過後的題目:

public class Book {

static int amount1 = 112;

static Book book = new Book(); // 入口1


public static void main(String[] args) {
staticFunction();
}


static {
System.out.println("書的靜態代碼塊");
}

{
System.out.println("書的普通代碼塊");
}

Book() {
System.out.println("書的構造方法");
System.out.println("price=" + price +", amount=" + amount + ", amount1=" + amount1);
}

public static void staticFunction() {
System.out.println("書的靜態方法");

     System.out.println("amount=" + amount + ",amount1=" + amount1);
}

int price = 110;
static int amount = 112;
// static Book book = new Book(); // 入口2
}

入口1 的結果

書的普通代碼塊
書的構造方法
price=110, amount=0, amount1=112
書的靜態代碼塊
書的靜態方法
amount=112, amount1=112

入口2 的結果

書的靜態代碼塊
書的普通代碼塊
書的構造方法
price=110, amount=112, amount1=112
書的靜態方法
amount=112, amount1=112

入口1 分析

在上面兩個例子中,因為 main 方法所在類並沒有多餘的代碼,我們都直接忽略瞭 main 方法所在類的初始化。

但在這個例子中,main 方法所在類有許多代碼,我們就並不能直接忽略瞭。

  1. 當 JVM 在準備階段的時候,便會為類變量分配內存和進行初始化。此時,我們的 book 實例變量被初始化為 null,amount,amout1 變量被初始化為 0。
  2. 當進入初始化階段後,因為 Book 方法是程序的入口,根據我們上面說到的類初始化的五種情況的第四種(當虛擬機啟動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類)。所以 JVM 會初始化 Book 類,即執行類構造器 。
  3. JVM 對 Book 類進行初始化首先是執行類構造器(按順序收集類中所有靜態代碼塊和類變量賦值語句就組成瞭類構造器 ),
  4. 後執行對象的構造器(按順序收集成員變量賦值和普通代碼塊, 最後收集對象構造器 ,最終組成對象構造器 )。

對於入口1,執行類構造器發現 book 實例是靜態變量,於是就會執行普通代碼塊,再去執行 book 的構造函數。執行完後,重新回到執行類構造器的路上,對剩下的靜態變量進行初始化。

入口2 分析

入口2 的變化就是將靜態實例初始化移到瞭最後。從而保證優先執行類構造器,再去進行對象初始化過程。

變例

假如把入口1,2 都註釋掉,這回結果會怎麼樣:

書的靜態代碼塊
書的靜態方法
amount=112, amount1=112

可以發現,最終隻有類構造器得到瞭執行。

方法論

從上面幾個例子可以看出,分析一個類的執行順序大概可以按照如下步驟:

  • 確定類變量的初始值。在類加載的準備階段,JVM 會為類變量初始化零值,這時候類變量會有一個初始的零值。如果是被 final 修飾的類變量,則直接會被初始成用戶想要的值。
  • 初始化入口方法。當進入類加載的初始化階段後,JVM 會尋找整個 main 方法入口,從而初始化 main 方法所在的整個類。當需要對一個類進行初始化時,會首先初始化類構造器(),之後初始化對象構造器()。
  • 初始化類構造器。JVM 會按順序收集類變量的賦值語句、靜態代碼塊,最終組成類構造器由 JVM 執行。
  • 初始化對象構造器。JVM 會按照收集成員變量的賦值語句、普通代碼塊,最後收集構造方法,將它們組成對象構造器,最終由 JVM 執行。

如果在初始化 main 方法所在類的時候遇到瞭其他類的初始化,那麼就先加載對應的類,加載完成之後返回。如此反復循環,最終返回 main 方法所在類。

讀者福利:

本人這邊專門整理瞭一套對應的Java面經pdf文檔復習資料,含全面的java知識點面試題,還有其他各類知識點的整理,特別適合一些即將面試或準備跳槽的Java開發者查缺補漏,需要獲取這套文檔資料的讀者朋友們可以關註小編,後臺私信關鍵字“面試資料”獲取這套Java面經pdf文檔資料

超易懂Java 類加載機制詳解,這波示例教程真的愛瞭

覺得此文不錯的大佬們可以多多關註或者幫忙轉發分享一下哦,感謝!!!!

超易懂Java 類加載機制詳解,這波示例教程真的愛瞭

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *