Java程序運(yùn)行時(shí),數(shù)據(jù)會(huì)分區(qū)存放功舀,JavaStack(Java棧)萍倡、 heap(堆)、method(方法區(qū))辟汰。
1列敲、Java棧
Java棧的區(qū)域很小,只有1M莉擒,特點(diǎn)是存取速度很快酿炸,所以在stack中存放的都是快速執(zhí)行的任務(wù),基本數(shù)據(jù)類型的數(shù)據(jù)涨冀,和對(duì)象的引用(reference)填硕。
駐留于常規(guī)RAM(隨機(jī)訪問(wèn)存儲(chǔ)器)區(qū)域。但可通過(guò)它的“棧指針”獲取處理的直接支持鹿鳖。棧指針若向下移扁眯,會(huì)創(chuàng)建新的內(nèi)存;若向上移翅帜,則會(huì)釋放那些內(nèi)存姻檀。這是一種特別快、特別有效的數(shù)據(jù)保存方式涝滴,僅次于寄存器绣版。創(chuàng)建程序時(shí),Java編譯器必須準(zhǔn)確地知道堆棧內(nèi)保存的所有數(shù)據(jù)的“長(zhǎng)度”以及“存在時(shí)間”歼疮。這是由于它必須生成相應(yīng)的代碼杂抽,以便向上和向下移動(dòng)指針。這一限制無(wú)疑影響了程序的靈活性韩脏,所以盡管有些Java數(shù)據(jù)要保存在棧里——特別是對(duì)象句柄缩麸,但Java對(duì)象并不放到其中。
JVM只會(huì)直接對(duì)JavaStack(Java棧)執(zhí)行兩種操作:①以幀為單位的壓椛氖福或出棧杭朱;②通過(guò)-Xss來(lái)設(shè)置阅仔, 若不夠會(huì)拋出StackOverflowError異常。
1.每個(gè)線程包含一個(gè)棧區(qū)弧械,棧中只保存基本數(shù)據(jù)類型的數(shù)據(jù)和自定義對(duì)象的引用(不是對(duì)象)八酒,對(duì)象都存放在堆區(qū)中
2.每個(gè)棧中的數(shù)據(jù)(原始類型和對(duì)象引用)都是私有的,其他棧不能訪問(wèn)梦谜。
3.棧分為3個(gè)部分:基本數(shù)據(jù)類型的變量區(qū)丘跌、執(zhí)行環(huán)境上下文、操作指令區(qū)(存放操作指令)唁桩。
棧是存放線程調(diào)用方法時(shí)存儲(chǔ)局部變量表闭树,操作,方法出口等與方法執(zhí)行相關(guān)的信息荒澡,Java棧所占內(nèi)存的大小由Xss來(lái)調(diào)節(jié)报辱,方法調(diào)用層次太多會(huì)撐爆這個(gè)區(qū)域。
2单山、程序計(jì)數(shù)器(ProgramCounter)寄存器
PC寄存器( PC register ):每個(gè)線程啟動(dòng)的時(shí)候碍现,都會(huì)創(chuàng)建一個(gè)PC(Program Counter,程序計(jì)數(shù)器)寄存器米奸。PC寄存器里保存有當(dāng)前正在執(zhí)行的JVM指令的地址昼接。 每一個(gè)線程都有它自己的PC寄存器,也是該線程啟動(dòng)時(shí)創(chuàng)建的悴晰。保存下一條將要執(zhí)行的指令地址的寄存器是 :PC寄存器慢睡。PC寄存器的內(nèi)容總是指向下一條將被執(zhí)行指令的地址,這里的地址可以是一個(gè)本地指針铡溪,也可以是在方法區(qū)中相對(duì)應(yīng)于該方法起始指令的偏移量漂辐。
3、本地方法棧
Nativemethodstack(本地方法棧):保存native方法進(jìn)入?yún)^(qū)域的地址棕硫。
4髓涯、堆
類的對(duì)象放在heap(堆)中,所有的類對(duì)象都是通過(guò)new方法創(chuàng)建哈扮,創(chuàng)建后纬纪,在stack(棧)會(huì)創(chuàng)建類對(duì)象的引用(內(nèi)存地址)。
一種常規(guī)用途的內(nèi)存池(也在RAM(隨機(jī)存取存儲(chǔ)器 )區(qū)域)滑肉,其中保存了Java對(duì)象育八。和棧不同:“內(nèi)存堆”或“堆”最吸引人的地方在于編譯器不必知道要從堆里分配多少存儲(chǔ)空間,也不必知道存儲(chǔ)的數(shù)據(jù)要在堆里停留多長(zhǎng)的時(shí)間赦邻。因此,用堆保存數(shù)據(jù)時(shí)會(huì)得到更大的靈活性实檀。要求創(chuàng)建一個(gè)對(duì)象時(shí)惶洲,只需用new命令編輯相應(yīng)的代碼即可按声。執(zhí)行這些代碼時(shí),會(huì)在堆里自動(dòng)進(jìn)行數(shù)據(jù)的保存恬吕。當(dāng)然签则,為達(dá)到這種靈活性,必然會(huì)付出一定的代價(jià):在堆里分配存儲(chǔ)空間時(shí)會(huì)花掉更長(zhǎng)的時(shí)間铐料。
JVM將所有對(duì)象的實(shí)例(即用new創(chuàng)建的對(duì)象)(對(duì)應(yīng)于對(duì)象的引用(引用就是內(nèi)存地址))的內(nèi)存都分配在堆上渐裂,堆所占內(nèi)存的大小由-Xmx指令和-Xms指令來(lái)調(diào)節(jié),sample如下所示:
public class HeapOOM {
static class OOMObject{}
/**
* @param args
*/
public static void main(String[] args) {
List list = new ArrayList();// List類和ArrayList類都是集合類钠惩,
// 但是ArrayList可以理解為順序表柒凉,
// 屬于線性表。
while (true) {
list.add(new OOMObject());
}
}
}
加上JVM參數(shù)-verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError篓跛,就能很快報(bào)出OOM:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
并且能自動(dòng)生成Dump膝捞。
5、方法區(qū)
method(方法區(qū))又叫靜態(tài)區(qū)愧沟,存放所有的①類(class)蔬咬,②靜態(tài)變量(static變量),③靜態(tài)方法沐寺,④常量和⑤成員方法林艘。
1.又叫靜態(tài)區(qū),跟堆一樣混坞,被所有的線程共享狐援。
2.方法區(qū)中存放的都是在整個(gè)程序中永遠(yuǎn)唯一的元素。這也是方法區(qū)被所有的線程共享的原因拔第。
(順便展開(kāi)靜態(tài)變量和常量的區(qū)別: 靜態(tài)變量本質(zhì)是變量咕村,是整個(gè)類所有對(duì)象共享的一個(gè)變量,其值一旦改變對(duì)這個(gè)類的所有對(duì)象都有影響蚊俺;常量一旦賦值后不能修改其引用懈涛,其中基本數(shù)據(jù)類型的常量不能修改其值。)
Java里面是沒(méi)有靜態(tài)變量這個(gè)概念的泳猬,不信你自己在某個(gè)成員方法里面定義一個(gè)static int i = 0批钠;Java里只有靜態(tài)成員變量。它屬于類的屬性得封。至于他放哪里埋心?樓上說(shuō)的是靜態(tài)區(qū)。我不知道到底有沒(méi)有這個(gè)翻譯忙上。但是深入JVM里是翻譯為方法區(qū)的拷呆。虛擬機(jī)的體系結(jié)構(gòu):①Java棧,② 堆,③PC寄存器茬斧,④方法區(qū)腰懂,⑤本地方法棧,⑥運(yùn)行常量池项秉。而方法區(qū)保存的就是一個(gè)類的模板绣溜,堆是放類的實(shí)例(即對(duì)象)的。棧是一般來(lái)用來(lái)函數(shù)計(jì)算的娄蔼。隨便找本計(jì)算機(jī)底層的書(shū)都知道了怖喻。棧里的數(shù)據(jù),函數(shù)執(zhí)行完就不會(huì)存儲(chǔ)了岁诉。這就是為什么局部變量每一次都是一樣的锚沸。就算給他加一后,下次執(zhí)行函數(shù)的時(shí)候還是原來(lái)的樣子唉侄。
方法區(qū)的大小由-XX:PermSize和-XX:MaxPermSize來(lái)調(diào)節(jié)咒吐,類太多有可能撐爆永久代。靜態(tài)變量或常量也有可能撐爆方法區(qū)属划。
6恬叹、運(yùn)行常量池
這兒的“靜態(tài)”是指“位于固定位置”。程序運(yùn)行期間同眯,靜態(tài)存儲(chǔ)的數(shù)據(jù)將隨時(shí)等候調(diào)用绽昼。可用static關(guān)鍵字指出一個(gè)對(duì)象的特定元素是靜態(tài)的须蜗。但Java對(duì)象本身永遠(yuǎn)都不會(huì)置入靜態(tài)存儲(chǔ)空間硅确。
這個(gè)區(qū)域?qū)儆诜椒▍^(qū)。該區(qū)域存放類和接口的常量明肮,除此之外菱农,它還存放成員變量和成員方法的所有引用。當(dāng)一個(gè)成員變量或者成員方法被引用的時(shí)候柿估,JVM就通過(guò)運(yùn)行常量池中的這些引用來(lái)查找成員變量和成員方法在內(nèi)存中的的實(shí)際地址循未。
7、舉例分析
例子如下:
為了更清楚地搞明白程序運(yùn)行時(shí)秫舌,數(shù)據(jù)區(qū)里的情況的妖,我們來(lái)準(zhǔn)備2個(gè)小道具(2個(gè)非常簡(jiǎn)單的小程序)。
// AppMain.java
public class AppMain { //運(yùn)行時(shí)足陨,JVM把AppMain的信息都放入方法區(qū)
public static void main(String[] args) { //main成員方法本身放入方法區(qū)嫂粟。
Sample test1 = new Sample( " 測(cè)試1 " ); //test1是引用,所以放到棧區(qū)里墨缘,Sample是自定義對(duì)象應(yīng)該放到堆里面
Sample test2 = new Sample( " 測(cè)試2 " );
test1.printName();
test2.printName();
}
}
// Sample.java
public class Sample { //運(yùn)行時(shí)星虹,JVM把a(bǔ)ppmain的信息都放入方法區(qū)零抬。
private name; //new Sample實(shí)例后,name引用放入棧區(qū)里搁凸,name對(duì)象放入堆里媚值。
public Sample(String name) {
this .name = name;
}
public void printName() {// printName()成員方法本身放入方法區(qū)里。
System.out.println(name);
}
}
OK护糖,讓我們開(kāi)始行動(dòng)吧,出發(fā)指令就是:“java AppMain”嚼松,包包里帶好我們的行動(dòng)向?qū)D嫡良。
系統(tǒng)收到了我們發(fā)出的指令,啟動(dòng)了一個(gè)Java虛擬機(jī)進(jìn)程献酗,這個(gè)進(jìn)程首先從classpath中找到AppMain.class文件寝受,讀取這個(gè)文件中的二進(jìn)制數(shù)據(jù),然后把Appmain類的類信息存放到運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)中罕偎。這一過(guò)程稱為AppMain類的加載過(guò)程很澄。
接著,JVM定位到方法區(qū)中AppMain類的Main()方法的字節(jié)碼颜及,開(kāi)始執(zhí)行它的指令甩苛。這個(gè)main()方法的第一條語(yǔ)句就是:
Sample test1 = new Sample("測(cè)試1");
語(yǔ)句很簡(jiǎn)單啦,就是讓JVM創(chuàng)建一個(gè)Sample實(shí)例俏站,并且呢讯蒲,使引用變量test1引用這個(gè)實(shí)例。貌似小case一樁哦肄扎,就讓我們來(lái)跟蹤一下JVM墨林,看看它究竟是怎么來(lái)執(zhí)行這個(gè)任務(wù)的:
1、Java虛擬機(jī)一看犯祠,不就是建立一個(gè)Sample類的實(shí)例嗎旭等,簡(jiǎn)單,于是就直奔方法區(qū)(方法區(qū)存放已經(jīng)加載的類的相關(guān)信息衡载,如類搔耕、靜態(tài)變量和常量)而去,先找到Sample類的類型信息再說(shuō)月劈。結(jié)果呢度迂,嘿嘿,沒(méi)找到@@猜揪,這會(huì)兒的方法區(qū)里還沒(méi)有Sample類呢(即Sample類的類信息還沒(méi)有進(jìn)入方法區(qū)中)惭墓。可JVM也不是一根筋的笨蛋而姐,于是腊凶,它發(fā)揚(yáng)“自己動(dòng)手,豐衣足食”的作風(fēng),立馬加載了Sample類钧萍, 把Sample類的相關(guān)信息存放在了方法區(qū)中褐缠。
2、Sample類的相關(guān)信息加載完成后风瘦。Java虛擬機(jī)做的第一件事情就是在堆中為一個(gè)新的Sample類的實(shí)例分配內(nèi)存队魏,這個(gè)Sample類的實(shí)例持有著指向方法區(qū)的Sample類的類型信息的引用(Java中引用就是內(nèi)存地址)。這里所說(shuō)的引用万搔,實(shí)際上指的是Sample類的類型信息在方法區(qū)中的內(nèi)存地址胡桨,其實(shí),就是有點(diǎn)類似于C語(yǔ)言里的指針啦~~瞬雹,而這個(gè)地址呢昧谊,就存放了在Sample類的實(shí)例的數(shù)據(jù)區(qū)中。
3酗捌、在JVM中的一個(gè)進(jìn)程中呢诬,每個(gè)線程都會(huì)擁有一個(gè)方法調(diào)用棧,用來(lái)跟蹤線程運(yùn)行中一系列的方法調(diào)用過(guò)程胖缤,棧中的每一個(gè)元素被稱為棧幀尚镰,每當(dāng)線程調(diào)用一個(gè)方法的時(shí)候就會(huì)向方法棧中壓入一個(gè)新棧幀。這里的幀用來(lái)存儲(chǔ)方法的參數(shù)草姻、局部變量和運(yùn)算過(guò)程中的臨時(shí)數(shù)據(jù)钓猬。OK,原理講完了撩独,就讓我們來(lái)繼續(xù)我們的跟蹤行動(dòng)敞曹!位于“=”前的test1是一個(gè)在main()方法中定義的變量,可見(jiàn)综膀,它是一個(gè)局部變量澳迫,因此,test1這個(gè)局部變量會(huì)被JVM添加到執(zhí)行main()方法的主線程的Java方法調(diào)用棧中剧劝。而“=”將把這個(gè)test1變量指向堆區(qū)中的Sample實(shí)例偎漫,也就是說(shuō)刘急,test1這個(gè)局部變量持有指向Sample類的實(shí)例的引用(即內(nèi)存地址)。
OK,到這里為止呢魂务,JVM就完成了這個(gè)簡(jiǎn)單語(yǔ)句的執(zhí)行任務(wù)胞锰。參考我們的行動(dòng)向?qū)D洞焙,我們終于初步摸清了JVM的一點(diǎn)點(diǎn)底細(xì)了猾愿,COOL!
接下來(lái)他巨,JVM將繼續(xù)執(zhí)行后續(xù)指令充坑,在堆區(qū)里繼續(xù)創(chuàng)建另一個(gè)Sample類的實(shí)例减江,然后依次執(zhí)行它們的printName()方法。當(dāng)JVM執(zhí)行test1.printName()方法時(shí)捻爷,JVM根據(jù)局部變量test1持有的引用辈灼,定位到堆中的Sample類的實(shí)例,再根據(jù)Sample類的實(shí)例持有的引用也榄,定位到方法區(qū)中Sample類的類型信息(包括①類巡莹,②靜態(tài)變量,③靜態(tài)方法手蝎,④常量和⑤成員方法)榕莺,從而獲取printName()成員方法的字節(jié)碼,接著執(zhí)行printName()成員方法包含的指令棵介。
虛擬機(jī)棧
棧區(qū):棧中分配的是基本類型和自定義對(duì)象的引用。
每個(gè)線程包含一個(gè)棧區(qū)吧史,棧中只保存基礎(chǔ)數(shù)據(jù)類型和自定義對(duì)象的引用(不是對(duì)象)邮辽,對(duì)象都存放在堆區(qū)中
每個(gè)棧中的數(shù)據(jù)(原始類型和對(duì)象引用)都是私有的,其他棧不能訪問(wèn)贸营。
棧分為3個(gè)部分:基本類型變量區(qū)吨述、執(zhí)行環(huán)境上下文、操作指令區(qū)(存放操作指令)钞脂。
棧是存放線程調(diào)用方法時(shí)存儲(chǔ)局部變量表揣云,操作,方法出口等與方法執(zhí)行相關(guān)的信息冰啃,棧大小由Xss來(lái)調(diào)節(jié)邓夕,方法調(diào)用層次太多會(huì)撐爆這個(gè)區(qū)域。
棧溢出一般只會(huì)出現(xiàn)無(wú)限循環(huán)的遞歸中阎毅,另外焚刚,線程太多也會(huì)占滿棧區(qū)域
棧幀: 一個(gè)完整的棧幀包含:局部變量表(基本數(shù)據(jù)類型變量),操作數(shù)棧扇调,動(dòng)態(tài)連接信息矿咕,方法完成和異常完成信息。
局部變量表概念和特征:
由若干個(gè)Slot組成狼钮,長(zhǎng)度由編譯期決定碳柱。
單個(gè)Slot可以存儲(chǔ)一個(gè)類型為boolean ,byte,char, short, float, reference和returnAddress的數(shù)據(jù),兩個(gè)Slot可以存儲(chǔ)一個(gè)類型為long或double的數(shù)據(jù)熬芜。
局部變量表用于方法間參數(shù)傳遞莲镣,以及方法執(zhí)行過(guò)程中存儲(chǔ)基礎(chǔ)數(shù)據(jù)類型的值和對(duì)象的引用。
本地方法棧:
本地方法棧的特征:
線程私有
后進(jìn)先出棧
作用是支撐Native方法的調(diào)用猛蔽,執(zhí)行和退出
可能出現(xiàn)OutOfMemoryError異常和StackOverflowError異常
java虛擬機(jī)棧和本地方法棸颍可能發(fā)生如下異常情況:
如果線程請(qǐng)求分配的棧容量超過(guò)Java虛擬機(jī)棧允許的最大容量時(shí)灵寺,Java虛擬機(jī)將會(huì)拋出一個(gè)StackOverflowError異常。
如果Java虛擬機(jī)可以動(dòng)態(tài)擴(kuò)展区岗,并且擴(kuò)展的動(dòng)作已經(jīng)嘗試過(guò)略板,但是目前無(wú)法申請(qǐng)到足夠的內(nèi)存去完成擴(kuò)展,或者在建立新的線程時(shí)沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧慈缔,那Java虛擬機(jī)將會(huì)拋出一個(gè)OutOfMemoryError異常叮称。
2)方法區(qū)
方法區(qū)是存放虛擬機(jī)加載類的相關(guān)信息,如類藐鹤、靜態(tài)變量和常量瓤檐,大小由-XX:PermSize和-XX:MaxPermSize來(lái)調(diào)節(jié),類太多有可能撐爆永久帶:
public class MethodAreaOOM {
static class OOMOjbect{}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
while(true){
Enhancer eh = new Enhancer();
eh.setSuperclass(OOMOjbect.class);
eh.setUseCache(false);
eh.setCallback(new MethodInterceptor(){
@Override
public Object intercept(Object arg0, Method arg1,
Object[] arg2, MethodProxy arg3) throws Throwable {
// TODO Auto-generated method stub
return arg3.invokeSuper(arg0, arg2);
}
});
eh.create();
}
}
}
加上永久帶的JVM參數(shù):-XX:PermSize=10M -XX:MaxPermSize=10M娱节,運(yùn)行后會(huì)報(bào)如下異常:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
靜態(tài)變量或常量也會(huì)有可能撐爆方法區(qū):
public class ConstantOOM {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
List<String> list = new ArrayList<String>();
int i=0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}
3)Java棧和本地方法棧
簡(jiǎn)單說(shuō)說(shuō)類加載過(guò)程挠蛉,里面執(zhí)行了哪些操作?
對(duì)類加載器有了解嗎肄满?
什么是雙親委派模型谴古?
雙親委派模型的工作過(guò)程以及使用它的好處。
虛擬機(jī)類加載機(jī)制的概念
虛擬機(jī)把描述類的數(shù)據(jù)從class文件加載到內(nèi)存稠歉,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)掰担、轉(zhuǎn)換解析和初始化。最終形成可以被虛擬機(jī)最直接使用的java類型的過(guò)程就是虛擬機(jī)的類加載機(jī)制怒炸。
Java語(yǔ)言的動(dòng)態(tài)加載和動(dòng)態(tài)連接
另外需要注意的很重要的一點(diǎn)是:java語(yǔ)言中類型的加載連接以及初始化過(guò)程都是在程序運(yùn)行期間完成的带饱,這種策略雖然會(huì)使類加載時(shí)稍微增加一些性能開(kāi)銷,但是會(huì)為java應(yīng)用程序提供高度的靈活性阅羹。java里天生就可以動(dòng)態(tài)擴(kuò)展語(yǔ)言特性就是依賴運(yùn)行期間動(dòng)態(tài)加載和動(dòng)態(tài)連接這個(gè)特點(diǎn)實(shí)現(xiàn)的勺疼。比如,如果編寫(xiě)一個(gè)面向接口的程序灯蝴,可以等到運(yùn)行時(shí)再指定其具體實(shí)現(xiàn)類恢口。
類加載時(shí)機(jī)
類從被加載到虛擬機(jī)內(nèi)存到卸出內(nèi)存為止,它的整個(gè)生命周期包括:
虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且只有五種情況必須立即對(duì)類進(jìn)行“初始化”:
使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候穷躁、讀取或設(shè)置一個(gè)類的靜態(tài)字段的時(shí)候耕肩,已經(jīng)調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。
使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候问潭,如果類沒(méi)有初始化猿诸,則需要先觸發(fā)其初始化。
當(dāng)初始化一個(gè)類的時(shí)候狡忙,如果發(fā)現(xiàn)其父類沒(méi)有被初始化就會(huì)先初始化它的父類梳虽。
當(dāng)虛擬機(jī)啟動(dòng)的時(shí)候,用戶需要指定一個(gè)要執(zhí)行的主類(就是包含main()方法的那個(gè)類)灾茁,虛擬機(jī)會(huì)先初始化這個(gè)類窜觉;
使用Jdk1.7動(dòng)態(tài)語(yǔ)言支持的時(shí)候的一些情況谷炸。
而對(duì)于接口,當(dāng)一個(gè)接口在初始化時(shí)禀挫,并不要求其父接口全部都完成了初始化旬陡,只有在真正使用到父接口時(shí)(如引用父接口中定義的常量)才會(huì)初始化。
所有引用類的方式都不會(huì)觸發(fā)初始化稱為被動(dòng)引用语婴,下面是3個(gè)被動(dòng)引用例子:
①通過(guò)子類引用父類靜態(tài)字段描孟,不會(huì)導(dǎo)致子類初始化;②通過(guò)數(shù)組定義引用類砰左,不會(huì)觸發(fā)此類的初始化
public class SuperClass {
static {
System.out.println("SuperClass(父類)被初始化了匿醒。。缠导。");
}
public static int value = 66;
}
public class Subclass extends SuperClass {
static {
System.out.println("Subclass(子類)被初始化了廉羔。。僻造。");
}
}
public class Test1 {
public static void main(String[] args) {
// 1:通過(guò)子類調(diào)用父類的靜態(tài)字段不會(huì)導(dǎo)致子類初始化
// System.out.println(Subclass.value);//SuperClass(父類)被初始化了蜜另。。嫡意。66
// 2:通過(guò)數(shù)組定義引用類,不會(huì)觸發(fā)此類的初始化
SuperClass[] superClasses = new SuperClass[3];
// 3:通過(guò)new 創(chuàng)建對(duì)象,可以實(shí)現(xiàn)類初始化捣辆,必須把1下面的代碼注釋掉才有效果不然經(jīng)過(guò)1的時(shí)候類已經(jīng)初始化了蔬螟,下面這條語(yǔ)句也就沒(méi)用了。
//SuperClass superClass = new SuperClass();
}
}
③常量在編譯階段會(huì)存入調(diào)用類的常量池中汽畴,本質(zhì)上并沒(méi)有直接引用定義常量的類旧巾,因此不會(huì)觸發(fā)定義常量的類的初始化
public class ConstClass {
static {
System.out.println("ConstClass被初始化了。忍些。鲁猩。");
}
public static final String HELLO = "hello world";
}
public class Test2 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO);//輸出結(jié)果:hello world
}
}
類加載過(guò)程
下面我們?cè)敿?xì)的說(shuō)一下java虛擬機(jī)中類加載的全過(guò)程:加載、驗(yàn)證罢坝、準(zhǔn)備廓握、解析和初始化這5個(gè)階段鎖執(zhí)行的具體工作。
1.加載嘁酿,“加載” 是 “類加載” 過(guò)程的一個(gè)階段隙券,切不可將二者混淆。
加載階段由三個(gè)基本動(dòng)作組成:
通過(guò)類型的完全限定名闹司,產(chǎn)生一個(gè)代表該類型的二進(jìn)制數(shù)據(jù)流(根本沒(méi)有指明從哪里獲取娱仔、怎樣獲取,可以說(shuō)一個(gè)非常開(kāi)放的平臺(tái)了)
解析這個(gè)二進(jìn)制數(shù)據(jù)流為方法區(qū)內(nèi)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
創(chuàng)建一個(gè)表示該類型的java.lang.Class類的實(shí)例游桩,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口牲迫。
通過(guò)類型的完全限定名耐朴,產(chǎn)生一個(gè)代表該類型的二進(jìn)制數(shù)據(jù)流的幾種常見(jiàn)形式:
從zip包中讀取,成為日后JAR盹憎、EAR筛峭、WAR格式的基礎(chǔ);
從網(wǎng)絡(luò)中獲取脚乡,這種場(chǎng)景最典型的應(yīng)用就是Applet;
運(yùn)行時(shí)計(jì)算生成蜒滩,這種場(chǎng)景最常用的就是動(dòng)態(tài)代理技術(shù)了;
由其他文件生成奶稠,比如我們的JSP俯艰;
注意: 非數(shù)組類加載階段既可以使用系統(tǒng)提供的類加載器來(lái)完成,也可以由用戶自定義的類加載器去完成锌订。(即重寫(xiě)一個(gè)類加載器的loadClass()方法)
2.驗(yàn)證
驗(yàn)證是連接階段的第一步竹握,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全辆飘。
虛擬機(jī)如果不檢查輸入的字節(jié)流啦辐,并對(duì)其完全信任的話,很可能會(huì)因?yàn)檩d入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰蜈项,所以驗(yàn)證是虛擬機(jī)對(duì)自身保護(hù)的一項(xiàng)重要工作芹关。這個(gè)階段是否嚴(yán)謹(jǐn),直接決定了java虛擬機(jī)是否能承受惡意代碼的攻擊紧卒。
從整體上看侥衬,驗(yàn)證階段大致上會(huì)完成4個(gè)階段的校驗(yàn)工作:文件格式、元數(shù)據(jù)跑芳、字節(jié)碼轴总、符號(hào)引用。
2.1文件格式驗(yàn)證
驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范博个,并且能被當(dāng)前版本的虛擬機(jī)處理怀樟。該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi)。這個(gè)階段驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的盆佣,只有通過(guò)這個(gè)階段的驗(yàn)證后往堡,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)進(jìn)行存儲(chǔ),所以后面的3個(gè)階段的全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的罪塔,不會(huì)再直接操作字節(jié)流投蝉。
2.2元數(shù)據(jù)驗(yàn)證
該階段對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求征堪,目的是保證不存在不符合Java語(yǔ)言規(guī)范的元數(shù)據(jù)信息瘩缆。
2.3字節(jié)碼驗(yàn)證
該階段主要工作時(shí)進(jìn)行數(shù)據(jù)流和控制流分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為佃蚜。 例如庸娱,保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上着绊、保證方法體中的類型轉(zhuǎn)換是有效的等等。
由于數(shù)據(jù)流校驗(yàn)的高復(fù)雜性熟尉,耗時(shí)較大归露,所以JDK1.6之后,在Javac中引入一項(xiàng)優(yōu)化方法(可以通過(guò)參數(shù)關(guān)閉):在方法體的Code屬性的屬性表中增加一項(xiàng)“StackMapTable”屬性斤儿,該屬性描述了方法體中所有基本塊開(kāi)始時(shí)本地變量表和操作棧應(yīng)有的狀態(tài)剧包,從而將字節(jié)碼驗(yàn)證的類型推導(dǎo)轉(zhuǎn)變?yōu)轭愋蜋z查從而節(jié)省一些時(shí)間。
注意: 如果一個(gè)方法體通過(guò)了字節(jié)碼驗(yàn)證往果,也不能說(shuō)明其一定是安全的疆液,因?yàn)樾r?yàn)程序邏輯無(wú)法做到絕對(duì)精確。
2.4符號(hào)引用驗(yàn)證
最后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候陕贮,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三個(gè)階段——解析階段中發(fā)生堕油。符號(hào)引用驗(yàn)證的目的是確保解析動(dòng)作能正常執(zhí)行。
驗(yàn)證的內(nèi)容主要有:
符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類肮之;
在指定類中是否存在符號(hào)方法的字段描述及簡(jiǎn)單名稱所描述的方法和字段掉缺;
符號(hào)引用中的類、字段和方法的訪問(wèn)性(private戈擒、protected眶明、public、default)是否可被當(dāng)前類訪問(wèn)筐高。
3.準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段赘来,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。(備注:這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量)凯傲,而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中)嗦篱。
初始值通常是數(shù)據(jù)類型的零值:
對(duì)于:public static int value = 123;冰单,那么變量value在準(zhǔn)備階段過(guò)后的初始值為0而不是123,這時(shí)候尚未開(kāi)始執(zhí)行任何java方法灸促,把value賦值為123的動(dòng)作將在初始化階段才會(huì)被執(zhí)行诫欠。
一些特殊情況:
對(duì)于:public static final int value = 123;編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為123浴栽。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程荒叼。
那么符號(hào)引用與直接引用有什么關(guān)聯(lián)呢?
4.1 看兩者的概念典鸡。
符號(hào)引用(Symbolic References): 符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo)被廓,符號(hào)可以是符合約定的任何形式的字面量,符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無(wú)關(guān)萝玷,引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中嫁乘。
直接引用(Direct References): 直接引用可以是直接指向目標(biāo)的指針昆婿、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)蜓斧,引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在仓蛆。
虛擬機(jī)規(guī)范沒(méi)有規(guī)定解析階段發(fā)生的具體時(shí)間,虛擬機(jī)實(shí)現(xiàn)可以根據(jù)需要來(lái)判斷到底是在類被加載時(shí)解析還是等到一個(gè)符號(hào)引用將要被使用前才去解析挎春。
4.2 對(duì)解析結(jié)果進(jìn)行緩存
同一符號(hào)引用進(jìn)行多次解析請(qǐng)求是很常見(jiàn)的看疙,除invokedynamic指令以外,虛擬機(jī)實(shí)現(xiàn)可以對(duì)第一次解析結(jié)果進(jìn)行緩存直奋,來(lái)避免解析動(dòng)作重復(fù)進(jìn)行能庆。無(wú)論是否真正執(zhí)行了多次解析動(dòng)作,虛擬機(jī)需要保證的是在同一個(gè)實(shí)體中帮碰,如果一個(gè)引用符號(hào)之前已經(jīng)被成功解析過(guò)相味,那么后續(xù)的引用解析請(qǐng)求就應(yīng)當(dāng)一直成功;同樣的殉挽,如果 第一次解析失敗丰涉,那么其他指令對(duì)這個(gè)符號(hào)的解析請(qǐng)求也應(yīng)該收到相同的異常。
4.3 解析動(dòng)作的目標(biāo)
解析動(dòng)作主要針對(duì)類或接口斯碌、字段一死、類方法、接口方法傻唾、方法類型投慈、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行。前面四種引用的解析過(guò)程冠骄,對(duì)于后面三種伪煤,與JDK1.7新增的動(dòng)態(tài)語(yǔ)言支持息息相關(guān),由于java語(yǔ)言是一門靜態(tài)類型語(yǔ)言凛辣,因此沒(méi)有介紹invokedynamic指令的語(yǔ)義之前抱既,沒(méi)有辦法將他們和現(xiàn)在的java語(yǔ)言對(duì)應(yīng)上。初始化
類初始化階段是類加載的最后一步扁誓,前面的類加載過(guò)程中防泵,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類加載器參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制蝗敢。到了初始化階段捷泞,才真正開(kāi)始執(zhí)行類中定義的java程序代碼(或者說(shuō)是字節(jié)碼)。
類加載器
1.類與類加載器
對(duì)于任意一個(gè)類寿谴,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性锁右。如果兩個(gè)類來(lái)源于同一個(gè)Class文件,只要加載它們的類加載器不同,那么這兩個(gè)類就必定不相等骡湖。
2.類加載器介紹
從Java虛擬機(jī)的角度分為兩種不同的類加載器:?jiǎn)?dòng)類加載器(Bootstrap ClassLoader) 和其他類加載器贱纠。其中啟動(dòng)類加載器,使用C++語(yǔ)言實(shí)現(xiàn)响蕴,是虛擬機(jī)自身的一部分谆焊;其余的類加載器都由Java語(yǔ)言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)之外浦夷,并且全都繼承自java.lang.ClassLoader類辖试。(這里只限于HotSpot虛擬機(jī))。
從Java開(kāi)發(fā)人員的角度來(lái)看劈狐,絕大部分Java程序都會(huì)使用到以下3種系統(tǒng)提供的類加載器罐孝。
啟動(dòng)類加載器(Bootstrap ClassLoader):
這個(gè)類加載器負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的肥缔,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別莲兢,如rt.jar,名字不符合的類庫(kù)即使放在lib目錄中也不會(huì)被加載)類庫(kù)加載到虛擬機(jī)內(nèi)存中续膳。
擴(kuò)展類加載器(Extension ClassLoader):
這個(gè)加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)改艇,它負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫(kù)坟岔,開(kāi)發(fā)者可以直接使用擴(kuò)展類加載器谒兄。
應(yīng)用程序類加載器(Application ClassLoader):
這個(gè)類加載器由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值社付,所以一般也稱它為系統(tǒng)類加載器承疲。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫(kù),開(kāi)發(fā)者可以直接使用這個(gè)類加載器鸥咖,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類加載器燕鸽,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
我們的應(yīng)用程序都是由這3種類加載器互相配合進(jìn)行加載的啼辣,如果有必要绵咱,還可以加入自己定義的類加載器。
3.雙親委派模型
雙親委派模型(Pattern Delegation Model),要求除了頂層的啟動(dòng)類加載器外熙兔,其余的類加載器都應(yīng)該有自己的父類加載器。這里父子關(guān)系通常是子類通過(guò)組合關(guān)系而不是繼承關(guān)系來(lái)復(fù)用父加載器的代碼艾恼。
雙親委派模型的工作過(guò)程: 如果一個(gè)類加載器收到了類加載的請(qǐng)求住涉,先把這個(gè)請(qǐng)求委派給父類加載器去完成(所以所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中),只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成加載請(qǐng)求時(shí)钠绍,子加載器才會(huì)嘗試自己去加載舆声。
使用雙親委派模型來(lái)組織類加載器之間的關(guān)系,有一個(gè)顯而易見(jiàn)的好處就是java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。
注意:雙親委派模型是Java設(shè)計(jì)者們推薦給開(kāi)發(fā)者們的一種類加載器實(shí)現(xiàn)方式媳握,并不是一個(gè)強(qiáng)制性 的約束模型碱屁。在java的世界中大部分的類加載器都遵循這個(gè)模型,但也有例外蛾找。
4.破壞雙親委派模型
雙親委派模型主要出現(xiàn)過(guò)3次較大規(guī)拿淦ⅲ“被破壞”的情況。
第一次破壞是因?yàn)轭惣虞d器和抽象類java.lang.ClassLoader在JDK1.0就存在的打毛,而雙親委派模型在JDK1.2之后才被引入柿赊,為了兼容已經(jīng)存在的用戶自定義類加載器,引入雙親委派模型時(shí)做了一定的妥協(xié):在java.lang.ClassLoader中引入了一個(gè)findClass()方法幻枉,在此之前碰声,用戶去繼承java.lang.Classloader的唯一目的就是重寫(xiě)loadClass()方法。JDK1.2之后不提倡用戶去覆蓋loadClass()方法熬甫,而是把自己的類加載邏輯寫(xiě)到findClass()方法中胰挑,如果loadClass()方法中如果父類加載失敗,則會(huì)調(diào)用自己的findClass()方法來(lái)完成加載椿肩,這樣就可以保證新寫(xiě)出來(lái)的類加載器是符合雙親委派模型規(guī)則的瞻颂。
第二次破壞是因?yàn)槟P妥陨淼娜毕荩F(xiàn)實(shí)中存在這樣的場(chǎng)景:基礎(chǔ)的類加載器需要求調(diào)用用戶的代碼覆旱,而基礎(chǔ)的類加載器可能不認(rèn)識(shí)用戶的代碼蘸朋。為此,Java設(shè)計(jì)團(tuán)隊(duì)引入的設(shè)計(jì)時(shí)“線程上下文類加載器(Thread Context ClassLoader)”扣唱。這樣可以通過(guò)父類加載器請(qǐng)求子類加載器去完成類加載動(dòng)作藕坯。已經(jīng)違背了雙親委派模型的一般性原則。
第三次破壞 是由于用戶對(duì)程序動(dòng)態(tài)性的追求導(dǎo)致的噪沙。這里所說(shuō)的動(dòng)態(tài)性是指:“代碼熱替換”炼彪、“模塊熱部署”等等比較熱門的詞。說(shuō)白了就是希望應(yīng)用程序能夠像我們的計(jì)算機(jī)外設(shè)一樣正歼,接上鼠標(biāo)辐马、U盤不用重啟機(jī)器就能立即使用。OSGi是當(dāng)前業(yè)界“事實(shí)上”的Java模塊化標(biāo)準(zhǔn)局义,OSGi實(shí)現(xiàn)模塊化熱部署的關(guān)鍵是它自定義的類加載器機(jī)制的實(shí)現(xiàn)喜爷。每一個(gè)程序模塊(OSGi中稱為Bundle)都有一個(gè)自己的類加載器,當(dāng)需要更換一個(gè)Bundle時(shí)萄唇,就把Bundle連同類加載器一起換掉以實(shí)現(xiàn)代碼的熱替換檩帐。在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹(shù)狀結(jié)構(gòu)另萤,而是進(jìn)一步發(fā)展為更加復(fù)雜的網(wǎng)狀結(jié)構(gòu)湃密。