當你在 Java 程序中new
對象時,有沒有考慮過 JVM 是如何把靜態的字節碼(byte code)轉化爲運行時對象的呢,這個問題看似簡單,但清楚的同窗相信也不會太多,這篇文章首先介紹 JVM 類初始化的機制,而後給出幾個易出錯的實例來分析,幫助你們更好理解這個知識點。javascript
JVM 將字節碼轉化爲運行時對象分爲三個階段,分別是:loading 、Linking、initialization。html
下面分別介紹這三個過程:java
Loading 過程主要工做是由ClassLoader
完成。該過程具體包括三件事:mysql
Class
對象的實例來表示該類JVM 中除了最頂層的Boostrap ClassLoader
是用 C/C++ 實現外,其他類加載器均由 Java 實現,咱們能夠用getClassLoader
方法來獲取當前類的類加載器:sql
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]複製代碼
Verification
主要是保證類符合 Java 語法規範,確保不會影響 JVM 的運行。包括但不限於如下事項:oracle
final
類沒有被繼承,final
方法沒有被覆蓋在一個類已經被load
而且經過verification
後,就進入到preparation
階段。在這個階段,JVM 會爲類的成員變量分配內存空間而且賦予默認初始值,須要注意的是這個階段不會執行任何代碼,而只是根據變量類型
決定初始值。若是不進行默認初始化,分配的空間的值是隨機的,有點類型c語言中的野指針問題。dom
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
的數據結構,它包含了指向全部類方法(也包括也從父類繼承的方法)的指針,這樣再調用父類方法時就不用再去搜索了。jvm
Resolution
階段主要工做是確認類、接口、屬性和方法在類run-time constant pool
的位置,而且把這些符號引用(symbolic references)替換爲直接引用(direct references)。ide
locating classes, interfaces, fields, and methods referenced symbolically from a type's constant pool, and replacing those symbolic references with direct references.
這個過程不是必須的,也能夠發生在第一次使用某個符號引用時。
通過了上面的load
、link
後,第一次
主動調用
某類的最後一步是Initialization
,這個過程會去按照代碼書寫順序進行初始化,這個階段會去真正執行代碼,注意包括:代碼塊(static與static)、構造函數、變量顯式賦值。若是一個類有父類,會先去執行父類的initialization
階段,而後在執行本身的。
上面這段話有兩個關鍵詞:第一次
與主動調用
。第一次
是說只在第一次時纔會有初始化過程,之後就不須要了,能夠理解爲每一個類有且僅有一次
初始化的機會。那麼什麼是主動調用
呢?
JVM 規定了如下六種狀況爲主動調用
,其他的皆爲被動調用
:
new
操做、反射、cloning
,反序列化)static
方法static
屬性進行賦值時(這不包括final
的與在編譯期肯定的常量表達式)main
方法的類)本文後面會給出一個示例用於說明主動調用
的被動調用
區別。
在這個階段,執行代碼的順序遵循如下兩個原則:
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
階段會根據顯示賦值的表達式再次進行賦值(按順序自上而下執行)。根據這兩點,就不難理解上面的結果了。
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("靜態塊");
}
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:靜態塊 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 三個過程。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:靜態塊 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 對類加載機制都有了更深的理解,若是你們仍是有疑問,歡迎留意討論。