本文將介紹Java虛擬機(jī)的基本結(jié)構(gòu),各組成部分的作用烁试,以及相互之間是如何協(xié)調(diào)的雇初。而要了解這些,首先必須了解Java堆减响、Java棧靖诗、永久區(qū)和元數(shù)據(jù)區(qū)的基本概念郭怪。
一、Java虛擬機(jī)的架構(gòu)
1.1 類加載子系統(tǒng)
類加載子系統(tǒng)負(fù)責(zé)從文件系統(tǒng)或者網(wǎng)絡(luò)中加載Class信息呻畸,加載的類信息放在一塊稱為方法區(qū)的內(nèi)存空間移盆。除了類的信息外,方法區(qū)中還會(huì)存放運(yùn)行時(shí)常量池的信息伤为,包括字符串字面量和數(shù)字常量(這部分常量信息是class文件中常量池部分的內(nèi)存映射)咒循。
1.2 程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,他可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器绞愚。在虛擬機(jī)的概念模型里(僅是概念模型叙甸,各種虛擬機(jī)可能會(huì)通過一些更高效的方式去實(shí)現(xiàn)),字節(jié)碼解釋器工作時(shí)就說通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令位衩,分支裆蒸、循環(huán)、跳轉(zhuǎn)糖驴、異常處理僚祷、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的贮缕,在任何一個(gè)確定的時(shí)刻辙谜,一個(gè)處理器(對于多核處理器來說是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此感昼,為了線程切換后能恢復(fù)到正確的執(zhí)行位置装哆,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響定嗓,獨(dú)立存儲(chǔ)蜕琴,我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個(gè)Java方法宵溅,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址:如果正在執(zhí)行的是Native方法凌简,這個(gè)計(jì)數(shù)器值則為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域恃逻。
1.3 Java虛擬機(jī)棧
Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的雏搂,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表辛块、操作數(shù)棧、動(dòng)態(tài)鏈接铅碍、方法出口等信息润绵。每一個(gè)方法從調(diào)用直至執(zhí)行完成,就對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程胞谈。
局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型(boolean尘盼、byte憨愉、char、short卿捎、int配紫、float、long午阵、double)躺孝、對象引用(reference類型,他不等同于對象本身底桂,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔樦才郏部赡苁侵赶蛞粋€(gè)代表對象的句柄或其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
在Java虛擬機(jī)規(guī)范中籽懦,對這個(gè)區(qū)域規(guī)定了兩種異常情況:如果線程請求的棧深度大于虛擬機(jī)所允許的深度于个,將拋出StackOverFlowError異常;如果虛擬機(jī)椖核常可以動(dòng)態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動(dòng)態(tài)擴(kuò)展厅篓,只不過Java虛擬機(jī)規(guī)范中也允許固定長度的虛擬機(jī)棧),如果擴(kuò)展時(shí)無法申請到足夠的內(nèi)存捶码,就會(huì)拋出OutOfMemoryError異常羽氮。
1.4 本地方法棧
與虛擬機(jī)棧的作用相似,他們之間的區(qū)別是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)宙项,而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)乏苦。
1.5 Java堆
Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域尤筐,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建汇荐。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存盆繁。
Java堆是垃圾收集器管理的主要區(qū)域掀淘,因此很多時(shí)候被稱為GC堆。由于現(xiàn)在收集器基本都采用分代收集算法油昂,所以Java堆中還可以細(xì)分為:新生代和老年代革娄;再細(xì)致一點(diǎn)的有Eden空間、From Survivor空間冕碟、To Survivor空間等拦惋。
1.6 方法區(qū)
與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域安寺,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息厕妖、常量、靜態(tài)變量挑庶、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)言秸。
1.7 運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分软能。Class文件中除了有類的版本、字段举畸、方法查排、接口等描述信息外,還有一項(xiàng)信息是常量池抄沮,用于存放編譯期生成的各種字面量和符號(hào)引用跋核,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
運(yùn)行時(shí)常量池相對于Class文件常量池的另外一個(gè)重要特征是ju'bei具備動(dòng)態(tài)性合是,Java語言并不要求常量一定只有編譯期才能產(chǎn)生了罪,也就是并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中聪全,這種特性被開發(fā)人員利用的比較多的便是String類的intern()方法泊藕。
1.8 直接內(nèi)存
直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域难礼。但是這部分內(nèi)存也被頻繁的使用娃圆,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。它直接在Java堆外蛾茉、直接向系統(tǒng)申請的內(nèi)存空間讼呢。通常,訪問直接內(nèi)存的速度會(huì)優(yōu)于Java堆谦炬。因此悦屏,在讀寫頻繁的場合可能會(huì)考慮使用直接內(nèi)存。由于直接內(nèi)存在Java堆外键思,因此它的大小不會(huì)直接受限于Xmxd指定的最大堆大小础爬,但是系統(tǒng)內(nèi)存是有限的,Java堆和直接內(nèi)存的總和依然受限于操作系統(tǒng)能給出的最大內(nèi)存吼鳞。
在JDK 1.4中新加入了NIO類看蚜,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,他可以使用Native函數(shù)庫直接分配堆外內(nèi)存赔桌,然后通過一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作供炎。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)疾党。
二音诫、認(rèn)識(shí)Java堆
Java堆是和Java應(yīng)用程序關(guān)系最為密切的內(nèi)存空間,幾乎所有的對象都存放在堆中雪位。并且Java堆是完全自動(dòng)化管理的竭钝,通過垃圾回收機(jī)制,垃圾對象會(huì)被自動(dòng)清理,而不需要顯式的釋放蜓氨。
根據(jù)垃圾回收機(jī)制的不同,Java堆有可能擁有不同的結(jié)構(gòu)队伟,最常見的一種是將Java堆分為新生代和老年代穴吹。其中,新生代存放新生對象或者年齡不大的對象嗜侮,老年代存放老年對象港令。新生代可能分為eden區(qū)、s0區(qū)锈颗、s1區(qū)顷霹,s0和s1也被稱為from和to區(qū)域,他們是兩塊大小相等击吱、可以互換角色的內(nèi)存空間淋淀。
在絕大多數(shù)情況下,對象首先分配在eden區(qū)覆醇,在一次新生代回收(Young GC)后朵纷,如果對象還存活,則會(huì)進(jìn)入s0或s1永脓,之后袍辞,沒經(jīng)過一次Young GC,對象如果存活常摧,他的年齡就會(huì)加1.當(dāng)對象的年齡達(dá)到一定條件后搅吁,就會(huì)被認(rèn)為是老年代,從而進(jìn)入老年代落午。
三板甘、出入Java棧
Java棧是一塊線程私有的內(nèi)存空間党瓮。如果說,Java堆和程序數(shù)據(jù)密切相關(guān)盐类,那么Java堆就是和線程執(zhí)行密切相關(guān)的寞奸。線程執(zhí)行的基本行為是函數(shù)調(diào)用,每次函數(shù)調(diào)用的數(shù)據(jù)都是通過Java棧傳遞的在跳。
Java棧與數(shù)據(jù)結(jié)構(gòu)上的棧有著類似的含義枪萄,他是一塊先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),只支持出棧和入棧兩種操作猫妙。Java虛擬機(jī)提供了參數(shù)-Xss來指定線程的最大棿煞空間,這個(gè)參數(shù)也直接決定了函數(shù)調(diào)用的最大深度。
3.1 局部變量表
局部變量表是棧幀的重要組成部分之一齐帚。它用于保存函數(shù)的參數(shù)以及局部變量妒牙。局部變量表中的變量只在當(dāng)前函數(shù)調(diào)用中有效,當(dāng)函數(shù)調(diào)用結(jié)束后对妄,隨著函數(shù)棧幀的銷毀湘今,局部變量表也會(huì)隨之銷毀。
由于局部變量表在棧幀之中剪菱,因此摩瞎,如果函數(shù)的參數(shù)和局部變量較多,會(huì)使得局部變量表膨脹孝常,從而每一次函數(shù)調(diào)用就會(huì)占用更多的椘烀牵空間,最終導(dǎo)致函數(shù)的嵌套調(diào)用次數(shù)減少构灸。
局部變量表中的變量也是重要的垃圾回收根節(jié)點(diǎn)上渴,只要被局部變量表中直接或間接引用的對象是不會(huì)被回收的。因此喜颁,理解局部變量表對理解垃圾回收也有一定的幫助驰贷。
可以使用參數(shù)-XX:+PrintGC,在輸出的日志中洛巢,可以看到垃圾回收前后堆的大小括袒。
3.2 操作數(shù)棧
它主要用于保存計(jì)算過程的中間結(jié)果,同時(shí)作為計(jì)算過程中變量臨時(shí)的存儲(chǔ)空間稿茉。
3.3 幀數(shù)據(jù)區(qū)
大部分Java字節(jié)碼指令需要進(jìn)行常量池訪問锹锰,在幀數(shù)據(jù)區(qū)中保存著訪問變量池的指針,方便程序訪問常量池漓库。同時(shí)異常處理表也是幀數(shù)據(jù)區(qū)中重要的一部分恃慧。
3.4 棧上分配
棧上分配是Java虛擬機(jī)提供的一項(xiàng)優(yōu)化技術(shù),他的基本思想是渺蒿,對于那些線程私有的對象(這里指不可能被其他線程訪問的對象)痢士,可以將他們打散分配在棧上,而不是分配在堆上茂装。分配在棧上的好處是可以在函數(shù)調(diào)用結(jié)束后自行銷毀怠蹂,而不需要垃圾回收器的介入,從而提高系統(tǒng)的性能少态。
棧上分配的一個(gè)技術(shù)基礎(chǔ)是進(jìn)行逃逸分析城侧。逃逸分析的目的是判斷對象的作用域是否有可能逃逸出函數(shù)體。
對于大量的零散小對象彼妻,棧上分配提供了一種很好的對象分配優(yōu)化策略嫌佑,棧上分配速度快豆茫,并且可以有效避免垃圾回收帶來的負(fù)面影響,但由于和堆空間相比屋摇,椏辏空間較小,因此對于大對象也不適合在棧上分配炮温。
四肤京、方法區(qū)
方法區(qū)是一塊所有線程共享的內(nèi)存區(qū)域,用于保存系統(tǒng)的類信息茅特,比如類的字段、方法棋枕、常量池等白修。方法去的大小決定了系統(tǒng)可以保存多少類,如果系統(tǒng)定義了太多的類重斑,導(dǎo)致方法區(qū)溢出兵睛,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出錯(cuò)誤。
在JDK 1.6窥浪、JDK 1.7中祖很,方法區(qū)可以理解為永久區(qū)(Perm)。永久區(qū)可以用參數(shù)-XX:PermSize和-XX:MaxPermSize指定漾脂,默認(rèn)情況下假颇,-XX:MaxPermSize為64M。一個(gè)大的永久區(qū)可以保存更多的類信息骨稿。如果系統(tǒng)使用了一些動(dòng)態(tài)代理笨鸡,那么有可能會(huì)在運(yùn)行時(shí)產(chǎn)生大量的類,如果這樣坦冠,就需要設(shè)置一個(gè)合理的永久區(qū)大小形耗,確保不發(fā)生永久區(qū)內(nèi)存溢出。
在JDK 1.8中辙浑,永久區(qū)已經(jīng)被徹底移除激涤。取而代之的是元數(shù)據(jù)區(qū),元數(shù)據(jù)區(qū)大小可以用參數(shù)-XX:MaxMetaspaceSize指定(一個(gè)大的元數(shù)據(jù)區(qū)可以使系統(tǒng)支持更多的類)判呕,這是一塊堆外的直接內(nèi)存倦踢。與永久區(qū)不同,如果不指定大小侠草,默認(rèn)情況下硼一,虛擬機(jī)會(huì)耗盡所有的可用系統(tǒng)內(nèi)存。