1.java類加載過程
重新回顧了java的類的生命周期,主要有:加載、鏈接、初始化、使用、卸載。上述過程包括了一個java類在jvm虛擬機中聲明周期的全過程。
其中,加載、鏈接、初始化,稱為類的加載過程。
而鏈接又包含了:驗證、準備、解析等過程。見下圖:
1.1加載
加載既是將class文件字節碼加載到內存中,并將這些靜態數據轉換為jvm方法區運行時數據結構。在堆中生成一個代表這個類的java.lang.Class對象,作為方法區訪問對象的入口。
1.2 鏈接
將已讀入內存的二進制數據合并到JVM運行狀態中去的過程。包含驗證、準備、解析等過程。
驗證:
1.類文件結構檢查:確保加載的類信息符合JVM規范,遵從類文件結構的固定格式。
2.語義檢查:確保類本身符合Java語言的語法規定,比如驗證final類型的類沒有子類,以及final類型的方法沒有被覆蓋。注意,語義檢查的錯誤在編譯器編譯階段就會通不過,但是如果有程序員通過非編譯的手段生成了類文件,其中有可能會含有語義錯誤,此時的語義檢查主要是防止這種沒有編譯而生成的class文件引入的錯誤。
3.字節碼驗證: 確保字節碼流可以被Java虛擬機安全地執行。
字節碼流代表Java方法(包括靜態方法和實例方法),它是由被稱作操作碼的單字節指令組成的序列,每一個操作碼后都跟著一個或多個操作數。
字節碼驗證步驟會檢查每個操作碼是否合法,即是否有著合法的操作數。
4.二進制兼容性驗證:確保相互引用的類之間的協調一致。例如,在Worker類的gotoWork()方法中會調用Car類的run()方法,Java虛擬機在驗證Worker類時,會檢查在方法區內是否存在Car類的run()方法,假如不存在(當Worker類和Car類的版本不兼容就會出現這種問題),就會拋出NoSuchMethodError錯誤。
準備:
正式為類變量(static變量)分配內存,并設置類變量初始值的階段。這些內存都將在方法區分配。
解析:
虛擬機常量池內的符號引用替換為直接引用的過程。
例如在Worker類的gotoWork()方法中會引用Car類的run()方法。
public void gotoWork() {
car.run();// 這段代碼在Worker類的二進制數據中表示為符號引用
}
在Worker類的二進制數據中,包含了一個對Car類的run()方法的符號引用,它由run()方法的全名和相關描述符組成。
在解析階段,Java虛擬機會把這個符號引用替換為一個指針,該指針指向Car類的run()方法在方法區內的內存位置,這個指針就是直接引用。
1.3 初始化
初始化是執行類的構造器<clinit>()方法的過程。
類構造器<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static塊)中的語句合并產生的。
當初始化一個類的時候,如果發現其父類還沒有進行過初始化、則需要先觸發其父類的初始化。
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確加鎖和同步。
當訪問一個java類的靜態域時,只有真正聲明這個域的類才會被初始化。
*說明 <clinit> 與<init>方法
可能出現在class文件中的兩種編譯器產生的方法是:實例初始化方法(名為<init>)和類與接口初始化方法(名為<clinit>)。
這兩個方法一個是虛擬機在裝載一個類初始化的時候調用的(clinit)。另一個是在類實例化時調用的(init)
<clinit>方法:所有的類變量初始化語句和類型的靜態初始化語句都被Java編譯器收集到了一起,放在一個特殊的方法中。這個方法就是<clinit>
<init>方法:是在一個類進行對象實例化時調用的。實例化一個類有四種途徑:調用new操作符;調用Class或java.lang.reflect.Constructor對象的newInstance()方法;調用任何現有對象的clone()方法;通過java.io.ObjectInputStream類的getObject()方法反序列化。Java編譯器會為它的每一個類都至少生成一個實例初始化方法。在Class文件中,被稱為"<init>"
區別:一個是用于初始化靜態的類變量, 一個是初始化實例變量!
1.4 使用
使用既是所需要的對象開始被調用。
1.5 卸載
對象被jvm回收。
示例
package com.dhb.classload;
public class InitDemo {
static {
System.out.println("InitDemo static init ...");
}
public static void main(String[] args) {
System.out.println("InitDemo main begin");
InitA a = new InitA();
System.out.println(InitA.width);
InitA b = new InitA();
}
}
class InitBase{
static {
System.out.println("InitBase static init ...");
}
}
class InitA extends InitBase {
public static int width = 60;
static {
System.out.println("InitA static init ...");
width = 30;
}
public InitA() {
System.out.println(" InitA init ... ");
}
}
運行結果:
InitDemo static init ...
InitDemo main begin
InitBase static init ...
InitA static init ...
InitA init ...
30
InitA init ...
可以看到,在執行結果中,先運行main方法所在類的初始化方法,之后運行main函數。然后運行父類InitBase的初始化方法。之后運行InitA的靜態初始化。以及InitA的構造函數。此后雖然new了多個InitA,但是其靜態的初始化方法<clinit>只運行了一次。
2.被動引用和主動引用
在java虛擬機規范中,嚴格規定了,只有對類進行主動引用,才會觸發其初始化方法。而除此之外的引用方式稱之為被動引用,不會觸發類的初始化方法。
2.1主動引用
虛擬機規范規定只有如下四種情況才能觸發主動引用:
2.1.1.遇到new、getstatic、setstatic、invokestatic 4條指令時,如果類沒有初始化,則需要觸發其初始化(final修飾的常量除外)。
(1).使用new關鍵字實例化對象
package com.dhb.classload;
public class NewClass {
static {
System.out.println("NewClass init ...");
}
}
class Init1{
public static void main(String[] args) {
new NewClass();
}
}
//輸出結果:
NewClass init ...
(2).讀取類的靜態成員變量
package com.dhb.classload;
public class StaticAttributeClass {
public static int value = 10;
public static void staticMethod() {
}
static {
System.out.println("StaticAttributeClass init ...");
}
}
class Init2{
public static void main(String[] args) {
//1.讀取靜態變量
int value = StaticAttributeClass.value;
}
}
//輸出結果
StaticAttributeClass init ...
(3).設置類的靜態成員變量
class Init2{
public static void main(String[] args) {
StaticAttributeClass.value = 5
}
}
//輸出結果
StaticAttributeClass init ...
(4).調用靜態方法
class Init2{
public static void main(String[] args) {
StaticAttributeClass.staticMethod();
}
}
//輸出結果
StaticAttributeClass init ...
2.1.2.使用java.lang.reflenct包的方法對類進行放射調用,如果沒有進行初始化,則需要觸發其初始化。
package com.dhb.classload;
public class ReflectClass {
static {
System.out.println("ReflectClass init ...");
}
}
class Init3{
public static void main(String[] args) {
try {
Class clazz = Class.forName("com.dhb.classload.ReflectClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//輸出結果
ReflectClass init ..
2.1.3.當一個類初始化的時候,如果其父類還沒有初始化,則需要先對其父類進行初始化。
package com.dhb.classload;
public class SuperClass {
static {
System.out.println("SuperClass init ...");
}
public static int value = 10;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init ...");
}
}
class Init4 {
public static void main(String[] args) {
new SubClass();
}
}
//輸出結果
SuperClass init ...
SubClass init ...
2.1.4.當虛擬機啟動時,用戶需要指定一個執行的主類,虛擬機會首先初始化這個主類
package com.dhb.classload;
public class MainClass {
static {
System.out.println("MainClass init ...");
}
public static void main(String[] args) {
System.out.println("main begin ...");
}
}
//輸出結果
MainClass init ...
main begin ...
2.2被動引用
主動引用之外的引用情況都稱之為被動引用,這些引用不會進行初始化。
2.2.1.通過子類引用父類的靜態字段,不會導致子類初始化
package com.dhb.classload;
public class SuperClass {
static {
System.out.println("SuperClass init ...");
}
public static int value = 10;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init ...");
}
}
class Init4 {
public static void main(String[] args) {
int value = SubClass.value;
}
}
//輸出結果
SuperClass init ...
2.2.2.通過數組定義來引用,不會觸發此類的初始化
package com.dhb.classload;
public class ArrayClass {
static {
System.out.println("ArrayClass init ...");
}
}
class Init5{
public static void main(String[] args) {
ArrayClass[] arrays = new ArrayClass[10];
}
}
//輸出結果為空
2.2.3.常量在編譯階段會存入調用類的常量池中,本質沒有直接引用到定義的常量類中,因此不會觸發定義的常量類初始化
package com.dhb.classload;
public class ConstClass {
static {
System.out.println("ConstClass init ...");
}
public static final int value = 10;
}
class Init6{
public static void main(String[] args) {
int value = ConstClass.value;
}
}
//輸出結果為空
2.3練習題
如下類的輸出:
package com.dhb.classload;
public class Singleton {
private static Singleton instance = new Singleton();
public static int x = 0;
public static int y;
private Singleton () {
x ++;
y ++;
}
public static Singleton getInstance() {
return instance;
}
public static void main(String[] args) {
Singleton singleton = getInstance();
System.out.println(x);
System.out.println(y);
}
}
上述類的執行結果為:
輸出結果竟然是 x為0 y為1 !!!
其實理解了類的加載過程也就不難理解,其過程如下:
(1).執行鏈接過程,初始化所有的類變量:
instance -> null
x -> 0
y -> 0
(2).執行初始化過程:
new Singleton() 調用構造方法
之后 x -> 1 y -> 1
再執行 x = 0 賦值
最終
x -> 0
y -> 1