由一道面試題理解類加載機制
不了解JVM的類加載機制你也可以coding,但是當你了解之後,可以讓你在coding的時候避免很多坑,本文將以一道常見的面試題去剖析一下。本文參考 深入理解Java虛擬機(第2版) 。
1public class ClassLoadTest { 2 private static ClassLoadTest test = new ClassLoadTest(); 3 4 static int x; 5 static int y = 0; 6 7 public ClassLoadTest() { 8 x++; 9 y++; 10 } 11 12 public static void main(String[] args) { 13 System.out.println(test.x); 14 System.out.println(test.y); 15 } 16}
這裏大家可以先猜測一下答案,可能結果會出乎你的意料~
類加載過程
先用一個圖簡單的描述一下類加載的這個過程
加載
這個過程相當於從本地或者網絡端去讀取一個字節流,然後將一些靜態儲存結構轉換成方法區中運行時期的數據,最後生成一個代表這個類的Class對象,作為方法區訪問這個類的入口。
例如:
-
咱們可以通過一個類的全限定名去加載類
-
通過jar、war包去加載類
-
通過http請求去第三方平臺上拉取指定的類來加載
- 運行時計算生成,例如Cglib動態代理等等
針對上述例子,這裏是加載一個?ClassLoadTest.class
?對象。
驗證
要理解這個環節並不是很難,一個東西要放到JVM上去運行,咱們肯定得對其進行一些過濾,不能啥都往上丟,這裏的驗證簡單的舉幾個例子:
-
文件格式的驗證:?
①是否以魔數0xCAFEBABE開頭;?
②主次版本號是否在當前虛擬機處理範圍內;?
③常量池中的常量是否有不被支持的常量類型等等。 -
元數據的驗證:?
①這個類是否有父類;?
②這個類的父類是否繼承了不被允許繼承的類(final修飾的類);?
③這個類不是抽象類,是否實現了所有接口中要實現的方法等等。 -
字節碼的驗證:
①保證跳轉指令不會跳轉到方法體以外的字節碼指令上;?
②保證方法體中的類型轉換是有效的等等。 - 符號引用的驗證:?
①能否通過類的全限定名去找到對應的類;?
②符號引用中的類、字段、方法是否可以被當前類訪問等等。
準備過程
這個過程相當於給類變量分配內存並設置變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。
針對上述例子:
1test = null;
2x = 0;
3y = 0;
註意:這裏有個特殊情況,如果該字段被?final
?修飾,那麽在準備階段改字段就會被設置成咱們自定義的值。?public static final int?value?= 11
?,在準備階段就會直接賦值11,並不是該變量的初始值。
解析過程
將符號引用轉換成直接引用的過程。這裏有兩個名詞?符號引用?和?直接引用?。
-
符號引用:符號引用與虛擬機的布局無關,甚至引用的目標不一定加載到了內存中。符號可以是任何形式的字面量,只要使用時能夠準確的定位到目標即可。
- 直接引用:直接引用可以直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用與虛擬機布局有關,如果有了直接引用,那麽引用的目標必定已經在內存中存在。
而解析過程又會針對類、字段、方法進行解析,解析失敗則會拋出相應的異常。例如在解析時發現沒有訪問權限會拋出?java.lang.IllegalAccessException
?異常,查詢不到引用字段會拋出?java.lang.NoSuchFieldException
?異常,查詢不到方法會拋出?java.lang.NoSuchMethodException
?異常等等。
初始化
在準備階段,變量已經賦值過系統要求的默認值,在初始化階段,則會根據程序制定的主觀計劃去初始化類變量和其他資源。這句話聽起來有些繞口,根據上述例子,實際上就是:
1test = new ClassLoadTest();// x = 1;y =1
2y = 0;
這個過程,由於?x
?咱們自己並沒有去設定一個值,所以初始化階段它不會發生任何改變,但是?y
?咱們有設定一個值0,所以最後造成最終結果為?x = 1;y = 0
?。
ps:在同一個類加載器下,一個類只會初始化一次。多個線程同時初始化一個類,只有一個線程能正常初始化,其他線程都會進行阻塞等待,直到活動線程執行初始化方法完畢。
總結
對於上面這個面試題,咱們用流程圖簡單的描述一下:
由一道面試題理解類加載機制