摘要:當你在程序中對象時,有沒有考慮過是如何把靜態(tài)的字節(jié)碼轉化為運行時對象的呢,這個問題看似簡單,但清楚的同學相信也不會太多,這篇文章首先介紹類初始化的機制,然后給出幾個易出錯的實例來分析,幫助大家更好理解這個知識點。
當你在 Java 程序中new對象時,有沒有考慮過 JVM 是如何把靜態(tài)的字節(jié)碼(byte code)轉化為運行時對象的呢,這個問題看似簡單,但清楚的同學相信也不會太多,這篇文章首先介紹 JVM 類初始化的機制,然后給出幾個易出錯的實例來分析,幫助大家更好理解這個知識點。
Loading, Linking, and InitializationJVM 將字節(jié)碼轉化為運行時對象分為三個階段,分別是:loading 、Linking、initialization。
下面分別介紹這三個過程:
LoadingLoading 過程主要工作是由ClassLoader完成。該過程具體包括三件事:
根據類的全名,生成一份二進制字節(jié)碼來表示該類
將二進制的字節(jié)碼解析成方法區(qū)對應的數據結構
最后生成一 Class 對象的實例來表示該類
JVM 中除了最頂層的Boostrap ClassLoader是用 C/C++ 實現外,其余類加載器均由 Java 實現,我們可以用getClassLoader方法來獲取當前類的類加載器:
public class ClassLoaderDemo { public static void main(String[] args) { System.out.println(ClassLoaderDemo.class.getClassLoader()); } } # sun.misc.Launcher$AppClassLoader@30a4effe # AppClassLoader 也就是上圖中的 System Class Loader
此外,我們在啟動java傳入-verbose:class來查看加載的類有那些。
java -verbose:class ClassLoaderDemo [Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.CharSequence from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] .... .... [Loaded java.security.BasicPermissionCollection from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] [Loaded ClassLoaderDemo from file:/Users/liujiacai/codes/IdeaProjects/mysql-test/target/classes/] [Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] sun.misc.Launcher$AppClassLoader@2a139a55 [Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]Linking Verification
Verification 主要是保證類符合 Java 語法規(guī)范,確保不會影響 JVM 的運行。包括但不限于以下事項:
bytecode 的完整性(integrity)
檢查final類沒有被繼承,final方法沒有被覆蓋
確保沒有不兼容的方法簽名
Preparation在一個類已經被load并且通過verification后,就進入到preparation階段。在這個階段,JVM 會為類的成員變量分配內存空間并且賦予默認初始值,需要注意的是這個階段不會執(zhí)行任何代碼,而只是根據變量類型決定初始值。如果不進行默認初始化,分配的空間的值是隨機的,有點類型c語言中的野指針問題。
Type Initial Value int 0 long 0L short (short) 0 char "u0000" byte (byte) 0 boolean false reference null float 0.0f double 0.0d
在這個階段,JVM 也可能會為有助于提高程序性能的數據結構分配內存,常見的一個稱為method table的數據結構,它包含了指向所有類方法(也包括也從父類繼承的方法)的指針,這樣再調用父類方法時就不用再去搜索了。
ResolutionResolution 階段主要工作是確認類、接口、屬性和方法在類run-time constant pool的位置,并且把這些符號引用(symbolic references)替換為直接引用(direct references)。
locating classes, interfaces, fields, and methods referenced symbolically from a type"s constant pool, and replacing those symbolic references with direct references.
這個過程不是必須的,也可以發(fā)生在第一次使用某個符號引用時。
Initialization經過了上面的load、link后,第一次 主動調用某類的最后一步是Initialization,這個過程會去按照代碼書寫順序進行初始化,這個階段會去真正執(zhí)行代碼,注意包括:代碼塊(static與static)、構造函數、變量顯式賦值。如果一個類有父類,會先去執(zhí)行父類的initialization階段,然后在執(zhí)行自己的。
上面這段話有兩個關鍵詞:第一次與主動調用。第一次是說只在第一次時才會有初始化過程,以后就不需要了,可以理解為每個類有且僅有一次初始化的機會。那么什么是主動調用呢?
JVM 規(guī)定了以下六種情況為主動調用,其余的皆為被動調用:
一個類的實例被創(chuàng)建(new操作、反射、cloning,反序列化)
調用類的static方法
使用或對類/接口的static屬性進行賦值時(這不包括final的與在編譯期確定的常量表達式)
當調用 API 中的某些反射方法時
子類被初始化
被設定為 JVM 啟動時的啟動類(具有main方法的類)
本文后面會給出一個示例用于說明主動調用的被動調用區(qū)別。
在這個階段,執(zhí)行代碼的順序遵循以下兩個原則:
有static先初始化static,然后是非static的
顯式初始化,構造塊初始化,最后調用構造函數進行初始化
示例 屬性在不同時期的賦值class Singleton { private static Singleton mInstance = new Singleton();// 位置1 public static int counter1; public static int counter2 = 0; // private static Singleton mInstance = new Singleton();// 位置2 private Singleton() { counter1++; counter2++; } public static Singleton getInstantce() { return mInstance; } } public class InitDemo { public static void main(String[] args) { Singleton singleton = Singleton.getInstantce(); System.out.println("counter1: " + singleton.counter1); System.out.println("counter2: " + singleton.counter2); } }
當mInstance在位置1時,打印出
counter1: 1 counter2: 0
當mInstance在位置2時,打印出
counter1: 1 counter2: 1
Singleton中的三個屬性在Preparation階段會根據類型賦予默認值,在Initialization階段會根據顯示賦值的表達式再次進行賦值(按順序自上而下執(zhí)行)。根據這兩點,就不難理解上面的結果了。
主動調用 vs. 被動調用class NewParent { static int hoursOfSleep = (int) (Math.random() * 3.0); static { System.out.println("NewParent was initialized."); } } class NewbornBaby extends NewParent { static int hoursOfCrying = 6 + (int) (Math.random() * 2.0); static { System.out.println("NewbornBaby was initialized."); } } public class ActiveUsageDemo { // Invoking main() is an active use of ActiveUsageDemo public static void main(String[] args) { // Using hoursOfSleep is an active use of NewParent, // but a passive use of NewbornBaby System.out.println(NewbornBaby.hoursOfSleep); } static { System.out.println("ActiveUsageDemo was initialized."); } }
上面的程序最終輸出:
ActiveUsageDemo was initialized. NewParent was initialized. 1
之所以沒有輸出NewbornBaby was initialized.是因為沒有主動去調用NewbornBaby,如果把打印的內容改為NewbornBaby.hoursOfCrying 那么這時就是主動調用NewbornBaby了,相應的語句也會打印出來。
首次主動調用才會初始化public class Alibaba { public static int k = 0; public static Alibaba t1 = new Alibaba("t1"); public static Alibaba t2 = new Alibaba("t2"); public static int i = print("i"); public static int n = 99; private int a = 0; public int j = print("j"); { print("構造塊"); } static { print("靜態(tài)塊"); } public Alibaba(String str) { System.out.println((++k) + ":" + str + " i=" + i + " n=" + n); ++i; ++n; } public static int print(String str) { System.out.println((++k) + ":" + str + " i=" + i + " n=" + n); ++n; return ++i; } public static void main(String args[]) { Alibaba t = new Alibaba("init"); } }
上面這個例子是阿里巴巴在14年的校招附加題,我當時看到這個題,就覺得與阿里無緣了。囧
1:j i=0 n=0 2:構造塊 i=1 n=1 3:t1 i=2 n=2 4:j i=3 n=3 5:構造塊 i=4 n=4 6:t2 i=5 n=5 7:i i=6 n=6 8:靜態(tài)塊 i=7 n=99 9:j i=8 n=100 10:構造塊 i=9 n=101 11:init i=10 n=102
上面是程序的輸出結果,下面我來一行行分析之。
由于Alibaba是 JVM 的啟動類,屬于主動調用,所以會依此進行 loading、linking、initialization 三個過程。
經過 loading與 linking 階段后,所有的屬性都有了默認值,然后進入最后的 initialization 階段。
在 initialization 階段,先對 static 屬性賦值,然后在非 static 的。k 第一個顯式賦值為 0 。
接下來是t1屬性,由于這時Alibaba這個類已經處于 initialization 階段,static 變量無需再次初始化了,所以忽略 static 屬性的賦值,只對非 static 的屬性進行賦值,所有有了開始的:
1:j i=0 n=0 2:構造塊 i=1 n=1 3:t1 i=2 n=2
接著對t2進行賦值,過程與t1相同
4:j i=3 n=3 5:構造塊 i=4 n=4 6:t2 i=5 n=5
之后到了 static 的 i 與 n:
7:i i=6 n=6
到現在為止,所有的static的成員變量已經賦值完成,接下來就到了 static 代碼塊
8:靜態(tài)塊 i=7 n=99
至此,所有的 static 部分賦值完畢,接下來是非 static 的 j
9:j i=8 n=100
所有屬性都賦值完畢,最后是構造塊與構造函數
10:構造塊 i=9 n=101 11:init i=10 n=102
經過上面這9步,Alibaba這個類的初始化過程就算完成了。這里面比較容易出錯的是第3步,認為會再次初始化 static 變量或代碼塊。而實際上是沒必要,否則會出現多次初始化的情況。
希望大家能多思考思考這個例子的結果,加深這三個過程的理解。
總結經過最后這三個例子,相信大家對 JVM 對類加載機制都有了更深的理解,如果大家還是有疑問,歡迎留意討論。
參考Java Virtual Machine Specification Chapter 5
Chapter 7 of Inside the Java Virtual Machine
JVM Internals
What kind of method is Constructor, static or non static?
Understanding the Java ClassLoader
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/66501.html
摘要:如果需要支持類的動態(tài)加載或需要對編譯后的字節(jié)碼文件進行解密操作等,就需要與類加載器打交道了。雙親委派模型,雙親委派模型,約定類加載器的加載機制。任何之類的字節(jié)碼都無法調用方法,因為該方法只能在類加載的過程中由調用。 jvm系列 垃圾回收基礎 JVM的編譯策略 GC的三大基礎算法 GC的三大高級算法 GC策略的評價指標 JVM信息查看 GC通用日志解讀 jvm的card table數據...
摘要:學習能更深入的理解這門語言,能理解語言底層的執(zhí)行過程,深入到字節(jié)碼層次。 目錄 ? 前言 程序的運行 1.JVM類加載機制 ①一般在什么情況下會去加載一個類?也就是說,什么時候.class字節(jié)碼文件中加載這個類到JVM內存里來? ②驗證、準備、初始化 ③初始化 2.類加載器和雙親委派機制 ...
摘要:前面提到,對于數組類來說,它并沒有對應的字節(jié)流,而是由虛擬機直接生成的。對于其他的類來說,虛擬機則需要借助類加載器來完成查找字節(jié)流的過程。驗證階段的目的,在于確保被加載類能夠滿足虛擬機的約束條件。 Java 虛擬機將字節(jié)流轉化為 Java 類的過程。這個過程可分為加載、鏈接以及初始化 三大步驟。 加載是指查找字節(jié)流,并且據此創(chuàng)建類的過程。加載需要借助類加載器,在 Java 虛擬機中,類...
摘要:作用負責將加載到中審查每個類由誰加載父優(yōu)先的等級加載機制將字節(jié)碼重新解析成統一要求的對象格式類結構分析為了更好的理解類的加載機制,我們來深入研究一下和他的方法。就算兩個是同一份字節(jié)碼,如果被兩個不同的實例所加載,也會認為它們是兩個不同。 申明:本文首發(fā)于 詳細深入分析 ClassLoader 工作機制 ,如有轉載,注明原出處即可,謝謝配合。 什么是 ClassLoader ? 大家...
閱讀 4727·2021-11-18 13:23
閱讀 905·2021-09-22 15:24
閱讀 1929·2021-09-06 15:00
閱讀 2634·2021-09-03 10:30
閱讀 1289·2021-09-02 15:15
閱讀 2079·2019-08-30 15:54
閱讀 3038·2019-08-30 15:44
閱讀 1460·2019-08-29 15:12