翻譯自jvm-works-jvm-architecture,並參考JVM 的類初始化機制。java
JVM(Java Virtual Machine)做爲一個運行時引擎去運行Java應用。JVM 是實際調用main方法的對象。JVM是JRE(Java Runtime Enviroment)的一部分。程序員
Java應用被稱爲WORA(Write Once Run Anywhere).這意味着程序員能夠在一個系統上編寫Java程序而且預期能夠不須要任何修改就運行在Java可以運行的系統中。這之因此可行就是由於JVM。bootstrap
它主要有如下幾個步驟:segmentfault
類加載器(class loader)讀取.class文件生成相應的二進制數據,並存儲在_方法區_中。對於每個.class文件,JVM存儲了一下幾個信息在方法區:數據結構
getName()
方法得到)在加載.class
文件後,JVM在_堆內存_中建立了一個類型爲Class的一個對象去表示這個文件。請注意,這個對象的類型是`java.lang`包中預約義的Class類型。這個Class對象能夠被用於獲取類級別的信息,如類名,父類名,方法和變量信息等等。咱們可使用Object類的_getClass()_ 去獲取對象的引用。dom
總結爲:jvm
// A Java program to demonstrate working of a Class type
// object created by JVM to represent .class file in
// memory.
import java.lang.reflect.Field;
import java.lang.reflect.Method;
// Java code to demonstrate use of Class object
// created by JVM
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
// Getting hold of Class object created
// by JVM.
Class c1 = s1.getClass();
// Printing type of object using c1.
System.out.println(c1.getName());
// getting all methods in an array
Method m[] = c1.getDeclaredMethods();
for (Method method : m)
System.out.println(method.getName());
// getting all fields in an array
Field f[] = c1.getDeclaredFields();
for (Field field : f)
System.out.println(field.getName());
}
}
// A sample class whose information is fetched above using
// its Class object.
class Student {
private String name;
private int roll_No;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getRoll_no() { return roll_No; }
public void setRoll_no(int roll_no) {
this.roll_No = roll_no;
}
}
複製代碼
輸出:函數
Student
getName
setName
getRoll_no
setRoll_no
name
roll_No
複製代碼
注意:每個加載的`.class`文件都只有一個Class對象被建立(即單例)性能
一般來講,有三種類加載器(Class loader):fetch
JVM 中除了最頂層的Boostrap ClassLoader
是用 C/C++ 實現外,其他類加載器均由 Java 實現,咱們能夠用getClassLoader
方法來獲取當前類的類加載器:
// Java code to demonstrate Class Loader subsystem
public class Test {
public static void main(String[] args) {
// String class is loaded by bootstrap loader, and
// bootstrap loader is not Java object, hence null
System.out.println(String.class.getClassLoader());
// Test class is loaded by Application loader
System.out.println(Test.class.getClassLoader());
}
}
複製代碼
Output:
null
sun.misc.Launcher$AppClassLoader@73d16e93
複製代碼
java -verbose:class Test
[Opened C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.io.Serializable from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Comparable from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.CharSequence from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.String from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.Type from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
...
[Loaded java.lang.Void from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
null
[Loaded Test from file:/D:/chenyue/Learn/jvm/target/classes/]
sun.misc.Launcher$AppClassLoader@73d16e93
[Loaded java.lang.Shutdown from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
複製代碼
注意:JVM遵循委託層級原則去加載類。系統類加載器將加載請求委託給擴展類加載器,擴展類加載器將請求委託給引導類加載器(boot-strap class loader)。若是這個類被髮如今引導路徑中,類將會被加載,除非請求被再次轉發給擴展類加載器,而後再轉發到系統加載器上。最後若是系統加載器加載類失敗,那咱們將會獲得一個運行時異常 java.lang.ClassNotFoundException.
執行驗證,準備和(可選)解決方案。
驗證(Verification):它保證`.class`文件的正確性即檢查文件是否被有效的編譯器正確格式化和生成。若是驗證失敗,咱們會獲得一個運行時異常_java.lang.VerigyError。_主要包含但不限於:
final
類沒有被繼承,final
方法沒有被覆蓋準備(Preparation):JVM爲類變量分配內存並初始化內存賦予默認值。
在這個階段,JVM 也可能會爲有助於提升程序性能的數據結構分配內存,常見的一個稱爲method table
的數據結構,它包含了指向全部類方法(也包括也從父類繼承的方法)的指針,這樣再調用父類方法時就不用再去搜索了。
解決(Resolution): 確認類、接口、屬性和方法在類run-time constant pool
的位置,用以將_符號引用(Symbolic References)_變爲直接引用。這經過搜索方法區來定位引用的實體來實現。 *
在這個階段,沒有的靜態變量和靜態代碼塊(若是存在)都被賦予定義在代碼中的數值。初始化的執行順序在一個類中是自頂向下的,在類的層次關係中是從父類到子類。
**
第一次 主動調用
**某類的最後一步是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 三個過程。
通過 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:靜態塊 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 變量或代碼塊。而其實是不必,不然會出現屢次初始化的狀況。
但願你們能多思考思考這個例子的結果,加深這三個過程的理解。
a. 加載類
b. 建立對象