👉 這是一個或許對你有用的社群
《專案實戰(影片)》:從書中學,往事上“練” 《網際網路高頻面試題》:面朝簡歷學習,春暖花開 《架構 x 系統設計》:摧枯拉朽,掌控面試高頻場景題 《精進 Java 學習指南》:系統學習,網際網路主流技術棧 《必讀 Java 原始碼專欄》:知其然,知其所以然

👉這是一個或許對你有用的開源專案國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERP、CRM、AI 大模型等等功能:
Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud 影片教程:https://doc.iocoder.cn 【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本
之前某次面試,我說自己對Java比較熟,面試官問了我一個問題:假設你自己寫一個String類,包名也是
java.lang
,程式碼裡使用String的時候,這個String類能編譯成功嗎?能執行成功嗎?好了,我當時又是一臉懵逼o((⊙﹏⊙))o,因為我只是看了些Java的面試題目,而且並沒有涉及類載入方面的內容(ps:我是怎麼敢說我對Java比較熟的)。
結論
先說結論:能編譯成功,但是執行會報錯。因為載入String的時候根據雙親委派機制會預設載入jdk裡的String。
在自己寫的String類中寫main方法並執行,會報錯找不到main方法。
publicclassString
{
publicintprint(int a)
{
int
b = a;
return
b;
}
publicstaticvoidmain(String[] args)
{
new
String().print(
1
);
}
}
上述程式碼執行報錯如下:
錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
publicstaticvoidmain(String[] args)
否則 JavaFX 應用程式類必須擴充套件javafx.application.Application
如果在其他類中嘗試呼叫這個String類的方法,也呼叫不到,實際的結果是呼叫jdk中的String類的方法。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/ruoyi-vue-pro 影片教程:https://doc.iocoder.cn/video/
題目分析
這裡涉及3個知識點:
-
Java程式碼的編譯過程 -
Java程式碼的執行過程 -
類載入器
以上3個內容基本上是涉及程式碼執行的整個流程了。接下來就結合實戰操作一步步分析具體的過程。
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/yudao-cloud 影片教程:https://doc.iocoder.cn/video/
Java程式碼的編譯過程
平時我都是透過IDEA直接執行程式碼,都沒注意過編譯的過程。所以結合平時的操作說明一下編譯的過程。
什麼是Java的編譯
Java的編譯過程,是將
.java
原始檔轉換為.class
位元組碼檔案的過程。如何將.java原始檔編譯成.class位元組碼檔案
IDEA工具中,點選
BUILD
按鈕
執行命令
javac xx.java
如何檢視位元組碼檔案
1、如果我們直接用文字工具開啟位元組碼檔案,將會看到以下內容:

這是因為Class檔案內部本質上是二進位制的,用不同的工具開啟看,展示的效果不一樣。下圖是用xx工具開啟的class檔案,展示的是十六進位制格式,其實可以自己一點點翻譯出來原始碼了。
class檔案的這個二進位制串,計算機是不能夠直接讀取並且執行的。也就是說,計算機看不懂,而我們的JVM解決了這個問題,JVM可以看作是一個翻譯官,它可以看懂,而且它也知道計算機想要什麼樣子的二進位制,所以它可以把Class檔案的二進位制翻譯成計算機需要的樣子

2、我們可以透過命令的方式將class檔案反彙編成彙編程式碼。
javap是JDK自帶的反彙編器,可以檢視java編譯器為我們生成的位元組碼。
javap -v xx
.
class
,
javap
-
c
-
lxx
.
class
位元組碼檔案中包含哪些內容
這個有很多文章說了,可以自己搜尋一下。
Java程式碼的執行過程
java類執行的過程大概可分為兩個過程:
-
類的載入; -
類的執行。
需要說明的是:JVM主要在程式第一次主動使用類的時候,才會去載入該類。也就是說,JVM並不是在一開始就把一個程式就所有的類都載入到記憶體中,而是到不得不用的時候才把它載入進來,而且只加載一次。
類載入過程
Class檔案需要載入到虛擬機器中之後才能執行和使用。系統載入Class檔案主要有3步:
載入
->連線
->初始化
。連線過程又可分為3步:驗證
->準備
->解析
。
載入
類載入過程的第一步,主要完成3件事情:
-
透過全類名獲取定義此類的二進位制位元組流。 -
將位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構 -
在記憶體中生成一個代表該類的Class物件,作為方法區這些資料的訪問入口。
載入這一步的操作主要是透過類載入器完成的。
每個Java類都有一個引用指向載入它的
ClassLoader
。不過陣列類不是透過ClassLoader
建立的,而是JVM在需要的時候自動建立的,陣列類透過getClassLoader
方法獲取ClassLoader
的時候和該陣列的元素型別的ClassLoader
是一致的。一個非陣列類的載入階段(載入階段獲取類的二進位制位元組流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類載入器去控制位元組流的獲取方式(重寫一個類載入器的
loadClass()
方法)。載入階段與連線階段的部分動作(如一部分位元組碼檔案格式驗證動作)是交叉進行的,載入階段尚未結束,連線階段可能就已經開始了。
連線
1.驗證
驗證是連線階段的第一步,這步的目的是為了保證Class檔案的位元組流中包含的資訊符合《Java虛擬機器規範》的全部約束要求,保證這些資訊被當做程式碼執行後不會危害虛擬機器的安全。
驗證階段所要耗費的資源相對還是多的,但驗證階段也不是必要的。如果程式執行的全部程式碼已經被反覆使用和驗證過,那在生產環境的實施階段可以考慮使用
-Xverify:none
引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。驗證階段主要由4個檢驗階段組成:
檔案格式驗證。 要驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。比如以下驗證點:
-
是否以魔數CAFEBABE開頭 -
主、次版本號是否在當前Java虛擬機器接收範圍內 -
常量池的常量是否有不被支援的常量型別
該階段驗證的主要目的是保證輸入的位元組流能夠被正確地解析並存儲於方法區。只有通過了這個階段的驗證之後,這段位元組流才被允許進入Java虛擬機器記憶體的方法區中儲存。後面3個階段的驗證是在方法區的儲存資訊上進行的,不會再直接讀取和操作位元組流了。
元資料驗證。 對位元組碼描述的資訊進行語義分析,保證其描述的資訊符合《Java語言規範》的要求。這個階段可能包括的驗證點如下:
-
這個類是否有父類(除了Object類之外,所有的類都應該有父類) -
這個類or其父類是否繼承了不允許繼承的類(比如final修飾的類) -
如果這個類不是抽象類,是否實現了其父類或介面之中要求實現的所有方法。
位元組碼驗證。 是整個驗證過程中最複雜的,主要目的是透過分析位元組碼,判斷位元組碼能否被正確執行。比如會驗證以下內容:
-
在位元組碼的執行過程中,是否會跳轉到一條不存在的指令 -
函式的呼叫是否傳遞了正確型別的引數 -
變數的賦值是不是給了正確的資料型別
如果一個方法體通過了位元組碼驗證,也仍然不能保證它一定是安全的。
符號引用驗證。 該動作發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將在連線的第三階段–解析階段中發生(所以說符號引用驗證是在解析階段發生???)。
符號引用驗證的主要目的是確保解析行為能正常執行。
符號引用驗證簡單來說就是驗證當前類是否缺少或者被禁止訪問它依賴的外部類、方法、變數等資源。該階段通常要校驗以下內容:
-
符號引用中透過字串描述的全限定名是否能找到對應的類。 -
在指定類中是否存在符合方法的欄位描述符及簡單名稱所描述的方法和欄位。(沒太明白什麼意思) -
符號引用中的類、變數、方法是否可被當前類訪問。
如果無法透過符號引用驗證,Java 虛擬機器將會丟擲一個
java.lang.IncompatibleClassChangeError
的子類異常,典型的如:-
java.lang.IllegalAccessError -
java.lang.NoSuchFieldError -
java.lang.NoSuchMethodError等。
2.準備
準備階段是正式為類中的靜態變數分配記憶體並設定類變數初始化值的階段。從概念上來說,這些變數所使用的記憶體都應當在方法區中分配,但方法區本身是一個邏輯概念。在JDK7及以前,HotSpot使用永久代來實現方法區。在JDK8及以後,類變數會隨著Class物件一起放入Java堆中(也是叫做方法區的概念?)
注意點:
a. 準備階段僅為類變數分配記憶體並初始化。例項變數會在物件例項化時隨著物件一起分配在堆記憶體中。
b. 非final修飾的類變數,在初始化之後,是賦值為0,而不是程式中的賦值。比如:
publicstaticint
value =
123
;
初始化之後的值是0,而不是10。因為這時候程式還未執行。把 value 賦值為 123 的
putstatic
指令是程式被編譯後,存放於類構造器方法之中,所以把 value 賦值為 123 的動作要到類的初始化階段才會被執行。c. final修飾的類變數,初始化之後會賦值為程式碼中的值。因為:如果類欄位被 final 修飾,那麼類阻斷的屬性表中存在
ConstantValue
屬性,那在準備階段變數值就會被初始化為ConstantValue
屬性所指定的初始值,假設上面類變數 value 的定義修改為 123 ,而不是 "零值"3.解析
解析階段是將符號引用轉化為直接引用的過程。也就是得到類或者欄位、方法在記憶體中的指標或者偏移量。
-
符號引用(Symbolic References): 用一組字串來表示所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。 -
直接引用(Direct Reference): 是可以直接指向目標的指標,相對偏移量、或者可以間接定位到目標的控制代碼?直接引用是和虛擬機器實現的記憶體佈局直接相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在虛擬機器的記憶體中存在。
初始化
初始化階段是執行初始化方法
<clinit> ()
方法的過程,是類載入的最後一步,這一步 JVM 才開始真正執行類中定義的 Java 程式程式碼(位元組碼)。說明:<clinit> ()
方法是編譯之後自動生成的。
對於初始化階段,虛擬機器嚴格規範了有且只有 6 種情況下,必須對類進行初始化(只有主動去使用類才會初始化類):
a. 當遇到
new
、 getstatic
、putstatic
或 invokestatic
這 4 條位元組碼指令時,比如 new 一個類,讀取一個靜態欄位(未被 final 修飾)、或呼叫一個類的靜態方法時。-
當 jvm 執行 new
指令時會初始化類。即當程式建立一個類的例項物件。 -
當 jvm 執行 getstatic
指令時會初始化類。即程式訪問類的靜態變數(不是靜態常量,常量會被載入到執行時常量池)。 -
當 jvm 執行 putstatic
指令時會初始化類。即程式給類的靜態變數賦值。 -
當 jvm 執行 invokestatic
指令時會初始化類。即程式呼叫類的靜態方法。
b. 使用
java.lang.reflect
包的方法對類進行反射呼叫時如 Class.forname("..."), newInstance()
等等。如果類沒初始化,需要觸發其初始化。c. 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。
d. 當虛擬機器啟動時,使用者需要定義一個要執行的主類 (包含 main 方法的那個類),虛擬機器會先初始化這個類。
e.
MethodHandle
和 VarHandle
可以看作是輕量級的反射呼叫機制,而要想使用這 2 個呼叫,就必須先使用findStaticVarHandle
來初始化要呼叫的類。f.當一個介面中定義了 JDK8 新加入的預設方法(default) ,那麼實現該介面的類需要提前初始化。
程式碼執行過程:案例
針對下面這段程式碼進行講解。
//MainApp.java
pblic
classMainApp
{
publicstaticvoidmain(String[] args)
{
Animal animal =
new
Animal(
"Puppy"
);
animal.printName();
}
}
//Animal.java
publicclassAnimal
{
public
String name;
publicAnimal(String name)
{
this
.name = name;
}
publicvoidprintName()
{
System.out.println(
"Animal ["
+name+
"]"
);
}
}
-
MainApp類載入:編譯得到 MainApp.class
檔案後,在命令列上敲java AppMain
。系統就會啟動一個jvm程序,jvm程序從classpath路徑中找到一個名為AppMain.class
的二進位制檔案,將MainApp的類資訊載入到執行時資料區的方法區內,這個過程叫做MainApp類的載入。 -
然後JVM找到AppMain的主函式入口,開始執行main函式。 -
Animal類載入:main函式的第一條命令是 Animal animal = new Animal("Puppy");
就是讓JVM建立一個Animal物件,但是這時候方法區中沒有Animal類的資訊,所以JVM馬上載入Animal類,把Animal類的型別資訊放到方法區中。 -
載入完Animal類之後,Java虛擬機器做的第一件事情就是在堆區中為一個新的Animal例項分配記憶體, 然後呼叫建構函式初始化Animal例項,這個Animal例項持有著指向方法區的Animal類的型別資訊(其中包含有方法表,java動態繫結的底層實現)的引用。 -
當使用 animal.printName()
的時候,JVM根據animal引用找到Animal物件,然後根據Animal物件持有的引用定位到方法區中Animal類的型別資訊的方法表,獲得printName()
函式的位元組碼的地址。 -
開始執行 printName()
函式。

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,“長按”或“掃描”下方二維碼噢:

星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。





文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)