一、運(yùn)行時(shí)數(shù)據(jù)區(qū)
我們?cè)诰帉慗ava程序時(shí)匣沼,使用JVM的流程主要如下所示:
虛擬機(jī)在執(zhí)行Java程序時(shí)狰挡,會(huì)把它所管理的內(nèi)存劃分為不同的數(shù)據(jù)區(qū)域,即運(yùn)行時(shí)數(shù)據(jù)區(qū)释涛。有些數(shù)據(jù)區(qū)域是線程共享的加叁,即這些區(qū)域會(huì)隨著虛擬機(jī)的啟動(dòng)而創(chuàng)建,隨著虛擬機(jī)的關(guān)閉而銷毀唇撬。而另一些區(qū)域則是與線程對(duì)應(yīng)它匕,屬于線程私有的。這些區(qū)域會(huì)隨著線程開始而創(chuàng)建窖认,隨著線程的結(jié)束而銷毀豫柬。
具體的劃分如下:
多個(gè)線程共享的:堆、方法區(qū)
每個(gè)線程私有的:程序計(jì)數(shù)器扑浸、Java虛擬機(jī)棧烧给、本地方法棧
圖示如下:
關(guān)于Java線程:在虛擬機(jī)中,每個(gè)線程都與操作系統(tǒng)的本地線程是直接對(duì)應(yīng)的喝噪。當(dāng)Java的線程準(zhǔn)備執(zhí)行時(shí)础嫡,操作系統(tǒng)的線程也同時(shí)創(chuàng)建了。
二酝惧、程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊很小的內(nèi)存空間驰吓,可以認(rèn)為是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,負(fù)責(zé)保存當(dāng)前線程正在執(zhí)行的字節(jié)碼地址系奉。
1檬贰、程序計(jì)數(shù)器的作用
① Java是支持多線程的,這也意味著CPU會(huì)不停地切換線程缺亮。虛擬機(jī)的多線程是通過CPU時(shí)間片輪轉(zhuǎn)法來實(shí)現(xiàn)的翁涤,即每個(gè)線程占用CPU的時(shí)間是同等的桥言,時(shí)間一到就會(huì)切換線程,如此重復(fù)葵礼,直到線程終止号阿。這也意味著某個(gè)線程會(huì)因?yàn)闀r(shí)間片到而被掛起,所以當(dāng)該線程再次獲得時(shí)間片時(shí)鸳粉,需要知道從哪個(gè)地方繼續(xù)執(zhí)行扔涧,此時(shí)虛擬機(jī)只需要讀取程序計(jì)數(shù)器的值就可以知道要執(zhí)行的字節(jié)碼指令的位置了。這也是程序計(jì)數(shù)器是線程私有的原因届谈,因?yàn)槿舫绦蛴?jì)數(shù)器不是線程私有的話枯夜,當(dāng)CPU切換線程時(shí),會(huì)按照上一個(gè)線程的字節(jié)碼指令的位置來執(zhí)行當(dāng)前線程的字節(jié)碼指令艰山,這顯然是不正確的
② JVM的字節(jié)碼解釋器也需要通過改變程序計(jì)數(shù)器的值來明確下一條字節(jié)碼指令湖雹。
2、程序計(jì)數(shù)器的特點(diǎn)
① 程序計(jì)數(shù)器是數(shù)據(jù)區(qū)中唯一沒有規(guī)定OutOfMemoryError
(內(nèi)存溢出異常)的區(qū)域曙搬。
②如果一個(gè)線程執(zhí)行的是Native本地方法摔吏,那么程序計(jì)數(shù)器的值為undefined。因?yàn)镴VM在執(zhí)行Native本地方法時(shí)纵装,是通過JNI調(diào)用本地其他語言來實(shí)現(xiàn)的征讲,而不是字節(jié)碼。
三橡娄、虛擬機(jī)棧
棧是運(yùn)行時(shí)的單位诗箍,堆是存儲(chǔ)的單位。
棧解決程序的運(yùn)行問題瀑踢,即方法如何執(zhí)行(或者如何處理數(shù)據(jù))。
堆解決的是數(shù)據(jù)存儲(chǔ)問題才避,即數(shù)據(jù)怎么放橱夭,在那放。
1桑逝、什么是虛擬機(jī)棧
虛擬機(jī)棧在每個(gè)線程開始時(shí)隨之創(chuàng)建棘劣,虛擬機(jī)棧與數(shù)據(jù)結(jié)構(gòu)中的棧一致,也是遵循先進(jìn)后出的原則楞遏。
虛擬機(jī)棧進(jìn)行出棧入棧操作的元素就是棧幀茬暇。每一個(gè)棧幀就對(duì)應(yīng)著一個(gè)方法,也可以將棧幀理解為一個(gè)方法的運(yùn)行空間寡喝。
一個(gè)棧幀入棧糙俗,意味著一個(gè)方法被調(diào)用,一個(gè)棧幀出棧预鬓,即一個(gè)方法執(zhí)行完畢巧骚。則棧幀的入棧順序就是方法的調(diào)用順序。
有代碼:
public class VMStackTest {
private static int i = 1;
public static void main(String[] args) {
VMStackTest test = new VMStackTest();
test.add();
System.out.println(i);
}
public void add(){
i++;
}
}
代碼所示的虛擬機(jī)棧示例如下:
同一時(shí)刻,在同一線程中劈彪,只有位于虛擬機(jī)棧頂部的棧幀才是有效運(yùn)行的竣蹦,即只有位于棧頂?shù)姆椒ㄊ钦趫?zhí)行的。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作沧奴。 如果在被調(diào)用的A方法中痘括,又調(diào)用了B方法,那么對(duì)應(yīng)的B方法的棧幀就會(huì)被創(chuàng)建滔吠,并被放在虛擬機(jī)棧的棧頂纲菌,成為新的正在執(zhí)行的方法。
對(duì)于虛擬機(jī)棧來說屠凶,不存在垃圾回收問題驰后。
Java方法有兩種方法返回方式,一種是正常的方法返回矗愧,使用return指令灶芝;另一種是拋出異常。不管是哪一種唉韭,都會(huì)導(dǎo)致棧幀被彈出夜涕。
2、棧幀的內(nèi)部結(jié)構(gòu)
每個(gè)棧幀都存儲(chǔ)著:
① 局部變量表
②操作數(shù)棧
③方法返回地址
④動(dòng)態(tài)鏈接
⑤一些附加信息
圖示如下:
(1)属愤、局部變量表
局部變量表定義為一個(gè)數(shù)字?jǐn)?shù)組女器,用于存放方法參數(shù)和方法內(nèi)定義的局部變量。
局部變量表建立在線程的虛擬機(jī)棧上住诸,是線程的私有數(shù)據(jù)驾胆,因此不存在數(shù)據(jù)安全問題。
虛擬機(jī)通過索引定位的方法查找相應(yīng)的局部變量贱呐,索引的范圍是從0~局部變量表最大容量丧诺。
局部變量表所需的容量大小是在編譯期定下的,方法運(yùn)行期間是不會(huì)該變局部變量表的大小的奄薇。
(2)驳阎、操作數(shù)棧
操作數(shù)棧也是一個(gè)棧的數(shù)據(jù)結(jié)構(gòu),操作數(shù)棧的最大深度也在編譯的時(shí)候定下馁蒂。
操作數(shù)棧的每一個(gè)元素可以是任意Java數(shù)據(jù)類型呵晚,在方法執(zhí)行過程中,操作數(shù)棧的深度都不會(huì)超過最大值沫屡。
操作數(shù)棧的作用是: 隨著方法執(zhí)行和字節(jié)碼指令的執(zhí)行饵隙,會(huì)從局部變量表或?qū)ο髮?shí)例的字段中復(fù)制常量或變量寫入到操作數(shù)棧,再隨著計(jì)算的進(jìn)行將棧中元素出棧到局部變量表或者返回給方法調(diào)用者沮脖,也就是出棧/入棧操作癞季。一個(gè)完整的方法執(zhí)行期間往往包含多個(gè)這樣出棧/入棧的過程劫瞳。
(3)、方法返回地址
一個(gè)方法結(jié)束绷柒,有正常執(zhí)行完畢退出和拋出異常(沒catch)志于,非正常退出兩種。
無論是那種方式废睦,在退出后都會(huì)返回到該方法被調(diào)用的位置伺绽。
正常退出時(shí),調(diào)用者的程序計(jì)數(shù)器的值會(huì)作為返回地址嗜湃,即調(diào)用該方法的指令的下一條指令的地址奈应。但是異常退出時(shí),返回地址要通過異常表來確定购披。
實(shí)際上杖挣,方法的退出就是當(dāng)前棧幀出棧的過程。故需要恢復(fù)上一層棧幀的局部變量表刚陡、操作數(shù)棧惩妇、將返回值壓入調(diào)用者棧幀的操作數(shù)棧,并設(shè)置其程序計(jì)數(shù)器的值等筐乳,讓調(diào)用者棧幀繼續(xù)執(zhí)行下去歌殃。
兩種方法結(jié)束的區(qū)別是:因拋出異常而退出的方法不會(huì)給調(diào)用他的調(diào)用者棧幀任何返回值
3、虛擬機(jī)棧的常見異常
《Java虛擬機(jī)規(guī)范》中蝙云,允許虛擬機(jī)棧的大小是動(dòng)態(tài)擴(kuò)展的或者固定不變的氓皱。
這意味著,虛擬機(jī)棧會(huì)出現(xiàn)兩種異常:
① StackOverflowError
異常(棧溢出):
如果線程請(qǐng)求分配的棧容量超過Java虛擬機(jī)的最大容量時(shí)勃刨,就會(huì)拋出棧溢出異常波材。最簡(jiǎn)單的示例就是不停遞歸而不退出,就會(huì)報(bào) StackOverflowError
身隐。
當(dāng)虛擬機(jī)棧的大小為固定時(shí)廷区,易出現(xiàn)該異常。
②OutOfMemoryError
異常(內(nèi)存溢出)
當(dāng)虛擬機(jī)棧采用動(dòng)態(tài)擴(kuò)展時(shí)抡医,一定程度上可以規(guī)避 StackOverflowError
躲因,但是虛擬機(jī)給每個(gè)線程分配到的內(nèi)存空間是有限的早敬,這也意味著隸屬于線程的虛擬機(jī)棧的內(nèi)存也是有限的忌傻,所以當(dāng)虛擬機(jī)棧請(qǐng)求擴(kuò)展的內(nèi)存大小無法滿足時(shí),就會(huì)報(bào)該異常搞监。
但是并不意味著水孩,采用固定大小的虛擬機(jī)棧就不會(huì)報(bào)該異常,如果某個(gè)棧幀太大琐驴,超出虛擬機(jī)棧所有的內(nèi)存也是可能的俘种。
四秤标、本地方法棧
(1)、本地方法
一個(gè)Native Method就是一個(gè)Java調(diào)用非Java代碼的接口宙刘。在定義一個(gè)本地方法時(shí)苍姜,并不提供實(shí)現(xiàn)體(有些就像定義一個(gè)Java interface),因?yàn)閷?shí)現(xiàn)體是由非Java語言在外面實(shí)現(xiàn)的悬包。本地接口的作用是融合不同的編程語言為Java所用衙猪。
(2)、本地方法棧
虛擬機(jī)棧是用于管理Java方法的調(diào)用布近,本地方法棧是用于管理本地方法的調(diào)用垫释。
①本地方法棧是線程私有的
②本地方法棧也是可實(shí)現(xiàn)固定或者可動(dòng)態(tài)擴(kuò)展的內(nèi)存大小(這也意味著本地方法棧的異常也與虛擬機(jī)棧一致)
③本地方法棧存放著被native關(guān)鍵字標(biāo)記的方法撑瞧,在執(zhí)行引擎執(zhí)行時(shí)加載本地方法庫棵譬。
④并不是所有的JVM都支持本地方法,故也無需實(shí)現(xiàn)本地方法棧
⑤在HotSpot JVM中预伺,直接將本地方法和虛擬機(jī)棧合二為一订咸。
五、擴(kuò)展--設(shè)置棧內(nèi)存大小
有代碼如下:
public class StackOverError {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
在代碼中采用了遞歸扭屁,以count++
的次數(shù)來查看目前虛擬機(jī)棧的深度算谈。其結(jié)果如下:
可見默認(rèn)情況下,
count
的值為2467料滥。我們可以使用
-Xss:來規(guī)定了每個(gè)線程虛擬機(jī)棧的內(nèi)存大小
如下然眼,
設(shè)置棧的大小:--Xss1m或者-Xss1k (設(shè)置方法IDEA:Run --> Edit Configuration --> VM option)
結(jié)果如下葵腹,
可見當(dāng)設(shè)置虛擬機(jī)棧的內(nèi)存大小為2m時(shí)高每,
count
的值為233424。
故可看出使用參數(shù) -Xss可以設(shè)置線程的最大椉纾空間鲸匿,且虛擬機(jī)棧的大小直接決定了函數(shù)調(diào)用的大小。
參考:
知乎-JVM虛擬機(jī)棧執(zhí)行原理深入詳解