先測試一番,全對的就走人
//題目一
class Parent1{
public static String parent1 = "hello parent1";
static { System.out.println("Parent1 靜態代碼塊"); }
}
class Children1 extends Parent1{
public static String children1 = "hello children1";
static {System.out.println("Children1 靜態代碼塊");}
}
//----------------------------------------------------------------
//題目二
class GrandParent2{
static { System.out.println("GrandParent2靜態代碼塊"); }
}
class Parent2 extends GrandParent2{
public static String parent2="hello parent2";
static{ System.out.println("Parent2 靜態代碼塊");}
}
class Children2 extends Parent2{
public static String children2 ="hello children2";
static{ System.out.println("Children2 靜態代碼塊");}
}
//----------------------------------------------------------------
//題目三
class GrandParent3{
static { System.out.println("GrandParent3靜態代碼塊"); }
}
class Parent3 extends GrandParent3{
public final static String parent3="hello parent3";
static{ System.out.println("Parent3 靜態代碼塊");}
}
class Children3 extends Parent3{
public static String children3 ="hello children3";
static{ System.out.println("Children3 靜態代碼塊");}
}
//測試
public class ClassLoaderTest {
public static void main(String[] args) {
//測試一的輸出
System.out.println(Children1.children1);
System.out.println("-------------------------------");
//測試二的輸出
System.out.println(Children2.parent2);
System.out.println("--------------------------------");
//測試三的輸出
System.out.println(Children3.parent3);
}
//你認為輸出什么呢
}
答案如下
Parent1 靜態代碼塊
Children1 靜態代碼塊
hello children1
GrandParent2靜態代碼塊
Parent2 靜態代碼塊
hello parent2
hello parent3
如果看清到這里,你的回答和結果一致,那么你真的懂了,可以轉載給他人了,如果出乎你的意料,請認真看完。
什么是類加載(或者初始化)
Java源代碼經過編譯之后轉換成class文件,在系統運行期間當需要某個類的時候,如果內存中還沒該class文件,那么JVM需要對這個類的class文件進行加載,連接,初始化,JVM通常會連續完成這三步,這個過程叫做類的加載或者初始化, 類從磁盤加載到內存必須經歷這三個階段的。
重點是:類的加載都是在程序運行期間完成的,這提供了無限可能,意味着你可以在某個階段對類的字節碼進行修改,JVM也確實提供了這樣的功能。
類的加載並不是對象的創建,類的加載是在為對象創建前做一些信息准備。
類的生命周期
我們明白了什么是類的加載,那么從類的加載到最后類的卸載成為類在JVM中的聲明周期,這個生命周期總共包含了七個階段:我畫一張圖,如下,我們逐個分析一下類的生命周期的每一步。
這是類的生命周期的,但它不總是按照這個固定的流程進行的,我們先知道這個就行,后面再說。
加載
類的加載指的是把class文件從磁盤讀入內存中,將其放入元數據區域並且創建一個Class對象,放入堆中,Class對象是類加載的最終產品,Class對象並不是new出來的對象。
元數據區域存儲的信息:
- 這個類型的完整有效名
- 這個類型的直接父類完整有效名
- 這個類型的修飾符(public final abstract等)
- 這個類型的直接接口的列表
Class對象中包含的如下信息,這也是我們能夠通過Class對象獲取類的很多信息的原因:
- 類的方法代碼,方法名,字段等
- 類的返回值
- 類的訪問權限
加載class文件有很多種方式,可以從磁盤上讀取,可以從網絡上讀取,可以從zip等歸檔文件中讀取,可以從數據庫中讀取
驗證
驗證的目的是驗證class文件的正確性,是否能夠被當前JVM虛擬機執行,主要包含了一些部分驗證,驗證非常重要,但不是必須的(正常情況下都是正確的)
文件格式驗證:比如JDK8加載的是JDK6下編譯的class文件,這肯定不行。
元數據驗證:確保字節碼描述信息符合Java語言規范的要求,你理解為校驗外殼,比如類中是否實現了接口的所有方法。
字節碼驗證:確定程序語義執行是合法的,校驗內在,校驗方法體,防止字節碼執行過程中危害JVM虛擬機。
符合引用驗證:其對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗,比如:符號引用中的類、字段、方法的訪問性是否可被當前類訪問,通過全限定名,是否能找到對應的類。
准備(重點)
驗證完成之后,JVM就開始為類變量(靜態變量) 分配內存,設置初始化值, 記住兩點
- 不會為成員變量分配內存的。
- 初始化值是指JVM默認的指,不是程序中指定的值。
看如下代碼,你就明白了:
//類變量,初始化值是 null, 不是123
public static String s1 = "123"
//成員變量
public String s2 = "456"
但有一個特殊,如果一個類變量是final修飾的常量,那么在准備階段就會被賦值為程序中指定的值,如下代碼,初始值是123
//初始值是123,不是null
public static final String s1 = "123"
為什么會這樣呢?兩行代碼的區別在於final,final在Java中代表着不可變,不能賦值了之后重新賦值,所以一開始就必須賦值為用戶想要的默認值,而不是Java語言的默認值。而不是final修時的變量有可能在之后發生變化,所以就先賦值為Java語言的默認值。
解析
解析階段主要是將常量池中的符號引用轉換為直接引用,解析動作主要包含類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用。
符號引用包括什么呢?
- 類和方法的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符,
直接引用是是什么呢?一個指向目標的指針地址或者句柄。
舉個例子如下:
// 123 是一個符號引用,123所對應的內存中的地址是一個直接引用。
public static final String s1 = "123"
常量池是什么呢?,常量池包含好多種,字符串常量池,class常量池,運行時常量池,這里指的是class常量池。我們寫的每一個Java類被編譯后,就會形成一份class文件,class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池,用於存放編譯器生成的各種字面量和符號引用,每個class文件都有一個class常量池。
比如解析階段,找不到某個字段就拋出NoSuchFieldError,同理NoSuchMethodError
初始化(重點)
初始化階段用戶定義的Java代碼才會真正開始執行,一般來說當首次主動使用某個類的時候就會對該類初始化,初始化某個類時也會初始化這個類的父類,這里的首次主動使用,大家要理解清楚了,第二次使用時不會初始化的。類的初始化其實就是執行類構造器的過程,這個不是我們代碼定義的構造方法。
下面列舉了JVM初始化類的時機:
- 創建對象時(比如:new Person())
- 訪問類變量時
- 調用類的靜態方法時
- 反射加載某個類是(Class.forName("....."))
- Java虛擬機啟動時被標明為啟動類的類(單測時),Main方法的類。
初始化時類變量會被賦予真正的值,也就是開發人員在代碼中定義的值,也會執行靜態代碼塊。
JVM初始化類的步驟:
- 若該類還沒有被加載和連接,則程序先加載並連接該類
- 若該類的父類還沒有初始化,則先初始化該類的夫類
- 若該類中有靜態代碼塊,則系統依次執行這些代碼塊
上面提到了首次主動使用時初始化類,那么就有被動使用,被動使用是什么意思呢?比如說通過子類引用父類的靜態字段,那么子類會初始化嗎?答案是不會的,所以下面測試的子類的靜態代碼塊是不會執行的。
class Parent4{
public final static String parent4="hello parent4";
}
class Children4 extends Parent4{
static{ System.out.println("Children4 靜態代碼塊");}
}
public class ClassLoaderTest {
public static void main(String[] args) {
//測試四的輸出
System.out.println(Children4.parent4);
}
}
再說一個點解析時有提到常量池的概念,在經過初始化后,類就被加載到內存中去了,這個時候jvm就會將class常量池中的內容存放到運行時常量池中,運行時常量池也是每個類都有一個。在解析階段,會把符號引用替換為直接引用,解析的過程會去查詢字符串常量池,以保證運行時常量池所引用的字符串與字符串常量池中是一致的
上面還有一個關鍵字一般來說,那么不一般呢?類加載器並不需要等到某個類被首次主動使用時再加載它,JVM規范允許類加載器在預料某個類將要被使用時就預先加載它.
使用
使用就比較簡單了,JVM初始化完成后,就開始按照順尋執行用戶代碼了。
卸載
類卸載有個前提,就是class的引用是空的,要么程序中手動置為空,要么進程退出時JVM銷毀class對象,然后JVM退出。只要class引用不存在,那么這個類就可以回收了。
你自己可以試驗一下,寫一個classload類加載器,寫一個Test測試類,實際測試一下,我的測試代碼如下:
public class ClassTest {
public static void main(String[] args){
ClassLoaderMy classLoader = new ClassLoaderMy();
classLoader.setRoot("D:\\github\\java_common\\target\\classes\\");
Class clazz = classLoader.findClass("jvm.Test類中有一個靜態代碼塊。");
Object obj = clazz.newInstance();
System.out.println("1:"+clazz.hashCode());
obj=null;
System.out.println("2:"+clazz.hashCode());
classLoader = null;
System.out.println("3:"+clazz.hashCode());
clazz = null;
System.out.println("此時 obj classloader clazz 都為空了");
classLoader = new ClassLoaderMy();
classLoader.setRoot("D:\\github\\java_common\\target\\classes\\");
clazz = classLoader.findClass("jvm.Test");
System.out.println("4:"+clazz.hashCode());
obj = clazz.newInstance();
}
//打印結果如下,看之前你猜一猜。Test類中有一個靜態代碼塊。
}
初始化了
1:1775282465
2:1775282465
3:1775282465
此時 obj classloader clazz 都為空了
4:1267032364
初始化了
最終結果你會發現,前三個hashcode的值是一樣的,第四個的值發生了變化,說明class文件被卸載了后重新加載生成了新的class對象,否則,同一個對象的hashcode是不會發生變化的,而且Test類的靜態代碼塊執行了兩遍,完整代碼地址如下:
https://github.com/sunpengwei1992/java_common/tree/master/src/jvm
我畫了一張圖,方便大家更好的理解,如下,當左邊的三個變量都指向為null時,最右邊的元數據區域的代表Class對象的Test二進制數據就會被卸載,當下次使用時就會被重新加載,初始化等。
但是,注意了 由JVM自帶的類加載器加載的類,在JVM生命周期中,始終不會被卸載,
JVM自帶的類加載器包括根類加載器,擴展類加載器,系統類加載器,這些回頭單聊。
解密測試題目
接下來我們聊一聊一開始的測試題,其實看到這里,想必大家都明白了吧,還是說一說。
第一個不用講了,都會。
第二題:子類Children2,父類Parent2, 祖父類GrandParent2,我們通過Chidlren2打印父類Parent2的靜態變量,類加載時,發現有父類存在,逐層往上加載,那么Parent2和GrandParent2都會被加載,所以Parent2和GrandParent2的靜態代碼塊都會被執行,而Children2就不會被加載了,因為不符合首次主動使用的條件。
第三題:同樣的道理,只是Parent3和GrandParent3的靜態代碼塊為什么沒執行呢,因為Parent3的靜態變量是final類型的,在准備階段就已經完成了,不需要再逐層往上加載了.
提一下接口的加載
當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,當真正用到父接口的時候才會加載該接口,如下代碼,執行main方法,Parent5接口是不會被加載的,parent5變量也是不會被初始化的。
interface Parent5{
public final static String parent5 = "hello parent5";
}
interface Children5 extends Parent5{
public final static String children5 = "hello children5";
}
public static void main(String[] args) {
System.out.println(Children5.children5);
}