1.前言
每當(dāng)我們編寫一個(gè)Java程序的時(shí)候都會(huì)經(jīng)歷厌丑,編寫击奶,編譯,運(yùn)行的一個(gè)過(guò)程。編譯的過(guò)程是通過(guò)Java的編譯器來(lái)幫我們完成馏段,他幫我們把java文件編譯成class二進(jìn)制進(jìn)制文件轩拨,并保存在硬盤中,但是當(dāng)我們要運(yùn)行程序的時(shí)候院喜,我們需要將class文件加載進(jìn)內(nèi)存亡蓉,啟動(dòng)JVM虛擬機(jī),虛擬機(jī)幫我們開(kāi)啟一個(gè)線程喷舀,來(lái)執(zhí)行我們編寫的代碼砍濒,而將字節(jié)碼文件加載進(jìn)內(nèi)存的過(guò)程就是需要ClassLoader,即類加載器。
在介紹類加載器之前我們先看一段代碼硫麻。
class Singleton {
private static Singleton singleton = new Singleton(); //1
public static int counter1; //2
public static int counter2 = 0; //3
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
public class ClassLoaderClient {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1 = " + singleton.counter1);
System.out.println("counter2 = " + singleton.counter2);
}
}
Singleton類很簡(jiǎn)單爸邢,聲明了3個(gè)靜態(tài)變量,對(duì)Singleton的構(gòu)造方法進(jìn)行了初始化拿愧,并且對(duì)Singleton進(jìn)行了單例化杠河。好讓我們思考一下在main方法中程序運(yùn)行的結(jié)果是什么?
很多朋友浇辜,可能會(huì)嗤之以鼻券敌,就會(huì)說(shuō),這還不簡(jiǎn)單柳洋,很顯然待诅,counter1,counter2 結(jié)果都為2⌒芰停可是結(jié)果真的是這樣嗎卑雁??我們看一下結(jié)果绪囱。
// 輸出結(jié)果
counter1 = 1
counter2 = 0
是不是出乎意料了序厉,嘿嘿,好玩的還在后面毕箍,我們將第1行代碼和第2,3行對(duì)調(diào),就像這樣道盏。
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
當(dāng)我們把程序修改成這樣之后而柑,程序的運(yùn)行結(jié)果就成這樣了。
// 輸出結(jié)果
counter1 = 1
counter2 = 1
我知道大家心里肯定都充滿了疑惑荷逞,為什么僅僅是更改了幾行代碼媒咳,輸出結(jié)果就發(fā)生了改變呢?帶著這些疑惑种远,我們學(xué)習(xí)完下面的內(nèi)容涩澡,“類加載”的相關(guān)概念,這個(gè)問(wèn)題就是小菜一碟啦坠敷!
2.類加載的過(guò)程概述
一般的妙同,一個(gè)類的加載要經(jīng)過(guò)三個(gè)過(guò)程射富,即 加載 => 連接 => 初始化。而每個(gè)過(guò)程都需要做一些事情粥帚,保證類能夠正確的加載進(jìn)內(nèi)存胰耗,讓我們可以正確的使用對(duì)象。
- 1.加載
查找并加載二進(jìn)制文件到內(nèi)存 - 2.連接
- 2.1驗(yàn)證:確保被加載類的正確性
- 2.2準(zhǔn)備:為類的靜態(tài)變量(類變量)分配內(nèi)存芒涡,并為其初始化為默認(rèn)值柴灯,基本數(shù)據(jù)類型是其默認(rèn)值,引用數(shù)據(jù)類型是null。
- 2.3解析:把類中的符號(hào)引用轉(zhuǎn)換為直接引用费尽。
- 3.初始化
為類的靜態(tài)變量賦予正確的初始值赠群。這里的初始值,和連接中的初始值旱幼,不是同一個(gè)初始值查描,這里的初始值,是我們自己為該靜態(tài)變量顯式的賦予初始值速警。
public static int counter1 = 10 // 將10賦值給counter1就是顯示的初始化
3.類加載分析
3.1加載
類的加載指的是類加載器將類的.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中叹誉,將其放在運(yùn)行時(shí)數(shù) 據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個(gè) java.lang.Class對(duì)象闷旧,用來(lái)封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)长豁,類的加載的最終產(chǎn)品是位于堆區(qū)中的 Class對(duì)象。Class對(duì)象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu) 忙灼,并且向Java程序員提供了訪問(wèn)方法區(qū)內(nèi) 的數(shù)據(jù)結(jié)構(gòu)的接口匠襟。Java中的反射機(jī)制就是操作堆內(nèi)存中的java.lang.Class對(duì)象。
3.2連接
類被加載后该园,就進(jìn)入連接階段酸舍。連接就是 將已經(jīng)讀入到內(nèi)存的類的二進(jìn)制數(shù)據(jù)合并 到虛擬機(jī)的運(yùn)行時(shí)環(huán)境中去。
3.2.1類的驗(yàn)證
驗(yàn)證就是對(duì)Class文件的語(yǔ)法,文法,格式規(guī)范進(jìn)行驗(yàn)證里初。放置惡意用戶對(duì)Class文件進(jìn)行修改啃勉,破壞程序的執(zhí)行。
驗(yàn)證主要包含以下幾個(gè)步驟双妨。
- 1類文件的結(jié)構(gòu)檢查
確保類文件遵從Java類文件的固定格式淮阐。 - 2語(yǔ)義檢查
確保類本身符合Java語(yǔ)言的語(yǔ)法規(guī)范,比如驗(yàn)證final的類不能被繼承刁品,final修飾的方法泣特,子類不能重寫等。 - 3字節(jié)碼驗(yàn)證
確保字節(jié)碼流可以被Java虛擬機(jī)安全地執(zhí)行挑随,字節(jié)碼流代表Java方法(包括實(shí)例方法和靜態(tài)方法),他是由被稱作操作嗎的單字節(jié)執(zhí)行組成的序列状您,每個(gè)操作碼后都跟隨一個(gè)或多個(gè)操作數(shù)(類似匯編語(yǔ)言)。字節(jié)碼驗(yàn)證步驟會(huì)檢查每個(gè)操作碼是否合法,即是否有著合法的操作數(shù)膏孟。 - 4二進(jìn)制兼容性的驗(yàn)證
確保相互引用的類之間的協(xié)調(diào)一致眯分,例如,Study類的getoStudy()方法調(diào)用Student里的studyEnglish()方法骆莹,Java虛擬機(jī)在驗(yàn)證Worker類的時(shí)候會(huì)檢查方法去內(nèi)是否存在Student類的StudyEnglish()方法颗搂,加入不存在(比如用Student類用Jdk1.6編譯,Study類Jdk1.5編譯)幕垦,就會(huì)拋出NoSuchMethodError錯(cuò)誤丢氢。
3.2.2類的準(zhǔn)備
在準(zhǔn)備階段,Java虛擬機(jī)為類的靜態(tài)變量分配內(nèi)存先改,并設(shè)置默認(rèn)的初始值疚察。例如下面的Sample類,在類的準(zhǔn)備階段仇奶,將int類型的靜態(tài)變量a分配4個(gè)子節(jié)的內(nèi)存空間貌嫡,并賦予默認(rèn)值0,為long類型的靜態(tài)變量b分配8個(gè)子節(jié)的內(nèi)存空間该溯,并賦予0
public class Sample {
private static int a = 1;
private static long b;
static {
b = 2;
}
}
3.2.3類的連接
在類的解析階段岛抄,Java虛擬機(jī)會(huì)把類的而精致數(shù)據(jù)種的符號(hào)引用替換為直接引用。例如Study類的gotoStudy方法會(huì)引用Student的studyEnglish()方法
public void gotoStudy() {
student.studyEnglish();
}
在Study類的二進(jìn)制數(shù)據(jù)中狈茉,包含對(duì)Student類studyEnglish()方法的符號(hào)引用夫椭,它由方法的全名和相關(guān)的描述符構(gòu)成,在解析階段氯庆,Java虛擬機(jī)會(huì)把這些符號(hào)引用用一個(gè)指針替換蹭秋,該指針指向Student類studyEnglish()方法在方法區(qū)內(nèi)的內(nèi)存位置,這個(gè)指針就是直接引用堤撵。
3.3初始化
在初始化階段仁讨,java虛擬機(jī)執(zhí)行類的初始化語(yǔ)句,為類的靜態(tài)變量賦予初始值实昨。在程序中靜態(tài)變量初始化由兩種途徑:(1)在靜態(tài)變量的聲明處進(jìn)行初始化 (2)在靜態(tài)代碼塊中進(jìn)行初始化洞豁。例如在以下代碼中靜態(tài)變量a,b就被顯示的初始化,而靜態(tài)變量c沒(méi)有被顯式初始化荒给,它將保持默認(rèn)值0族跛。
public class Sample {
private static int a = 1;
private static long b;
private static long c;
static {
b = 2;
}
}
3.3.1類初始化步驟
- 加入這個(gè)類未被加載和連接,那就先進(jìn)行加載和連接锐墙。
- 加入這個(gè)類存在直接的父類,并且這個(gè)父類沒(méi)有初始化长酗,那么就先初始化父類溪北。
- 假如類中存在初始化語(yǔ)句,那就依此執(zhí)行這些初始化語(yǔ)句。
3.3.2類初始化的時(shí)機(jī)
而在Java中Class文件的加載還是有條件的并不是,想怎么加載就怎么加載之拨,這樣的話可就亂了套了茉继。因此JVM對(duì)Class文件的加載時(shí)機(jī)做了說(shuō)明。
3.3.2.1Java程序?qū)︻惖氖褂梅譃閮煞N方式蚀乔。
- 主動(dòng)使用(6種方式)
所有的Java虛擬機(jī)加載Class文件,必須在每個(gè)類或接口被Java程序“首次主動(dòng)使用”時(shí)才初始化他們烁竭。- 創(chuàng)建類的實(shí)例。
- 初始化一個(gè)類的子類吉挣。
- 訪問(wèn)某個(gè)類或接口的靜態(tài)變量派撕,或者對(duì)該靜態(tài)變量賦值。
- 調(diào)用類的靜態(tài)方法睬魂。
- 反射,如 Class.forName("com.test.user")
- Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類,就是通過(guò)java命令執(zhí)行包含main方法的類终吼,如 java Test
// 1.創(chuàng)建類的實(shí)例
User user = new User();
// 2.初始化一個(gè)類的子類
Class Father {}
Class Son extends Father {}
Son son = new Son();
// 3.訪問(wèn)類/接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值
String var1 = StaticClassTest.COUNTER1;
// 4.調(diào)用類的靜態(tài)方法
StaticClassTest.method1();
// 5.反射
Class<?> aClass = Class.forName("com.test.jvm.AJvm.classLoaderSeq.context.Son");
- 被動(dòng)使用
除了以上六種情況氯哮,其他使用Java類的方 式都被看作是對(duì)類的被動(dòng)使用际跪,都不會(huì)導(dǎo) 致類的初始化
3.3.2.2一些注意事項(xiàng)
-
1.接口的初始化
當(dāng)Java虛擬機(jī)初始化一個(gè)類的時(shí)候,要求它的父類都已經(jīng)被初始化喉钢,但這個(gè)規(guī)則不適用于接口姆打。- 在初始化一個(gè)類時(shí),并不會(huì)初始化它所實(shí)現(xiàn)的接口肠虽。
- 在初始化一個(gè)接口的時(shí)候幔戏,并不會(huì)初始化它的父接口。
因此舔痕,一個(gè)父接口并不會(huì)它的子接口或者實(shí)現(xiàn)類初始化而初始化评抚,只有程序首次使用特定接口的靜態(tài)變量。才會(huì)導(dǎo)致該接口的初始化伯复。
2.編譯時(shí)常量
當(dāng)一個(gè)類中變量被 final static 修飾成為靜態(tài)常量時(shí),我們?cè)偃ブ鲃?dòng)調(diào)用該靜態(tài)常量時(shí),類的初始化的過(guò)程就是另一番的結(jié)果慨代。
class Test1 {
static final int cnt = 6 /3;
static final int times = new Random().nextInt(100);
static {
System.out.println("Test Const ClassLoader!");
}
}
public class ConstClassLoaderTest {
public static void main(String[] args) {
System.out.println("value=" + Test1.cnt);
}
}
輸出結(jié)果value=2
,根據(jù)我們前面學(xué)習(xí)的知識(shí)點(diǎn),應(yīng)對(duì)類進(jìn)行初始化啸如,也就是說(shuō)先輸出靜態(tài)代碼塊的內(nèi)容再輸出 value=2,因?yàn)閏nt被final修飾侍匙,因此程序的運(yùn)行結(jié)果會(huì)有些不一樣。
對(duì)于靜態(tài)常量cnt來(lái)說(shuō)叮雳,java在編譯階段就可以確定該變量的具體數(shù)值 因此不需要將該類進(jìn)內(nèi)存想暗。而我們把這種靜態(tài)變量叫做編譯時(shí)常量。
當(dāng)把代碼修改成這樣是程序的運(yùn)行結(jié)果又會(huì)不一樣帘不。
public class ConstClassLoaderTest {
public static void main(String[] args) {
System.out.println("value=" + Test1.times);
}
}
運(yùn)行結(jié)果
Test Const ClassLoader!
value=41
對(duì)于靜態(tài)常量times來(lái)說(shuō),java在編譯階段無(wú)法確定該變量的具體數(shù)值,需要在運(yùn)行時(shí)才能確定说莫, 需要將該類加載進(jìn)內(nèi)存進(jìn)行,初始化后確定該常量的具體數(shù)值寞焙。
- 3.只有當(dāng)程序訪問(wèn)的靜態(tài)變量或靜態(tài)方法確 實(shí)在當(dāng)前類或當(dāng)前接口中定義時(shí)储狭,才可以 認(rèn)為是對(duì)類或接口的主動(dòng)使用
如何理解這句話呢互婿?我們還是看代碼說(shuō)話。
class Parent1 {
static int cnt1 = 1;
static {
System.out.println("Parent1初始化!");
}
static void doSomeThing() {
System.out.println("Parent1.doSomeThng()!");
}
}
class Son1 extends Parent1 {
static {
System.out.println("Son1初始化辽狈!");
}
}
public class TestClassLoader {
public static void main(String[] args) {
System.out.println(Son1.cnt1);
Son1.doSomeThing();
}
}
輸出結(jié)果
Parent1初始化!
cnt1=1
Parent1.doSomeThng()!
為什么Son1的靜態(tài)代碼塊沒(méi)有執(zhí)行呢慈参?
因?yàn)閏nt1是從父類繼承下來(lái)的。并非在當(dāng)前類(Son類)定義的靜態(tài)成員/方法,因此Son的靜態(tài)代碼不會(huì)執(zhí)行刮萌。
4.結(jié)果分析
看完了這些內(nèi)容我們?cè)俜治鲆粋€(gè)剛開(kāi)始的那個(gè)類的執(zhí)行結(jié)果驮配。
class Singleton {
private static Singleton singleton = new Singleton(); //1
public static int counter1; //2
public static int counter2 = 0; //3
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
public class ClassLoaderClient {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1 = " + singleton.counter1);
System.out.println("counter2 = " + singleton.counter2);
}
}
- 由"java虛擬機(jī)會(huì)加載被聲明為啟動(dòng)類的類"這條規(guī)則可以知道,JVM啟動(dòng)時(shí)首先會(huì)把ClassLoaderClient類加載進(jìn)內(nèi)存着茸。
- 程序執(zhí)行ClassLoaderClient.main()方法的第一條語(yǔ)句Sington.getInstance()獲取Singtone類的一個(gè)唯一實(shí)例壮锻。但是getInstance()方法是靜態(tài)方法,所以JVM會(huì)將Sington類加載進(jìn)內(nèi)存元扔。會(huì)經(jīng)歷加載 => 連接 => 初始化
- 在[連接]中的[準(zhǔn)備階段]躯保,會(huì)對(duì)Sington中靜態(tài)變量!靜態(tài)變量!靜態(tài)變量!進(jìn)行默認(rèn)的初始化,賦值情況如下澎语。
private static Singleton singleton = null;
public static int counter1 = 0;
public static int counter2 = 0;
- 然后在初始化階段對(duì)程序進(jìn)行默認(rèn)初始化途事,此時(shí)代碼中的第一行調(diào)用構(gòu)造函數(shù)對(duì)Sington實(shí)例進(jìn)行初始化。結(jié)果就是
public static int counter1 = 1;
public static int counter2 = 1;
- 最后執(zhí)行第2擅羞,3行代碼,counter1未對(duì)其進(jìn)行賦值尸变,所以保持其構(gòu)造方法對(duì)它的賦值1,counter2 則進(jìn)行了顯式的賦值,此時(shí) counter2為0
因此最終結(jié)果是。
// 輸出結(jié)果
counter1 = 1
counter2 = 0
現(xiàn)在我們?cè)賮?lái)分析第二種情況减俏,將第1行代碼和第2召烂,3行代碼對(duì)調(diào)。
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
其實(shí)前三步的結(jié)果是一樣的就是從第四部開(kāi)始有些不一樣了娃承。
- 在初始化階段奏夫,類的初始化順序和靜態(tài)變量/靜態(tài)代碼的聲明順序是保持一致的,因此程序的執(zhí)行流程是這樣的历筝。
- counter1保持它的默認(rèn)初始值0
- counter2被顯式的賦值為0(注意:是初始化階段的賦值酗昼,并非準(zhǔn)備階段的默認(rèn)值)
- 調(diào)用new Singleton()構(gòu)造方法對(duì)counter1賦值為1,counter2賦值1
因此最終結(jié)果是
// 輸出結(jié)果
counter1 = 1
counter2 = 1