Java程序運行機制與JVM
Java編寫的程序需要經(jīng)過編譯盖彭,但編譯不會生成特定平臺的機器碼枫振,而是生成一種與平臺無關(guān)只面向JVM的字節(jié)碼(即.class文件)柑营。這種字節(jié)碼必須使用Java解釋器來解釋執(zhí)行氨淌,Java里負責解釋執(zhí)行字節(jié)碼文件就是JVM泊愧。JVM是可運行Java字節(jié)碼文件的虛擬計算機。
JVM =?類加載器 +?執(zhí)行引擎 +?運行時數(shù)據(jù)區(qū)域盛正。
類加載器將Class文件加載到JVM中的運行時數(shù)據(jù)區(qū)域删咱,執(zhí)行引擎負責執(zhí)行字節(jié)碼文件。
Java內(nèi)存分配的粗糙分法
Java把內(nèi)存劃分成兩種:棧內(nèi)存與堆內(nèi)存豪筝√底蹋基本類型變量和對象的引用變量都是在函數(shù)的棧內(nèi)存中分配,而堆內(nèi)存用來存放由new創(chuàng)建的對象和數(shù)組壤蚜。在堆中產(chǎn)生了一個數(shù)組或?qū)ο笾蠹垂眩梢栽跅V卸x一個特殊的變量,讓棧中的這個變量的取值等于數(shù)組或?qū)ο笤诙褍?nèi)存中的首地址袜刷,棧中的這個變量就成了數(shù)組或?qū)ο蟮囊米兞看细弧R米兞吭诔绦蜻\行到其作用域之外后被釋放(其實就類似于局部變量的釋放)。數(shù)組和對象在沒有引用變量指向它的時候變?yōu)槔罚陔S后的一個不確定的時間被垃圾回收器收走墩蔓。
Java內(nèi)存分配的準確分法
Java虛擬機所管理的內(nèi)存包括以下幾個運行時數(shù)據(jù)區(qū)域。
程序計數(shù)器
程序計數(shù)器(線程私有)是一塊較小的內(nèi)存空間萧豆,它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器奸披。由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,因此為了線程切換后能恢復(fù)到正確的執(zhí)行位置涮雷,每條線程都需要有個獨立的程序計數(shù)器阵面。
虛擬機棧
Java虛擬機棧(線程私有)的生命周期與線程相同。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧样刷、動態(tài)鏈接仑扑、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程置鼻,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程镇饮。
經(jīng)常有人把Java內(nèi)存區(qū)分為堆內(nèi)存和棧內(nèi)存,這種分法比較粗糙箕母。其中所指的"棧"就是現(xiàn)在講的虛擬機棧储藐,或者說是虛擬機棧中局部變量表部分。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型和對象引用和returnAddress類型(指向了一條字節(jié)碼指令的地址)嘶是。局部變量表所需的內(nèi)存空間在編譯期間完成分配钙勃,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的俊啼,在方法運行期間不會改變局部變量表的大小肺缕。
在Java虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度授帕,將拋出StackOverflowError異常同木;如果虛擬機棧可以動態(tài)擴展(當前大部分的Java虛擬機都可動態(tài)擴展跛十,只不過Java虛擬機規(guī)范中也允許固定長度的虛擬機棧)彤路,如果擴展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常芥映。
本地方法棧
本地方法棧(線程私有)與虛擬機棧所發(fā)揮的作用是非常相似的洲尊,它們之間的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機使用到的Native方法服務(wù)奈偏。有的虛擬機(Hotspot)直接就把本地方法棧和虛擬機棧合二為一坞嘀。與虛擬機棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常惊来。
堆
對于大多數(shù)應(yīng)用來說丽涩,Java堆(也叫做"GC堆")是Java虛擬機所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域裁蚁,在虛擬機啟動時創(chuàng)建矢渊。此內(nèi)存區(qū)域的唯一目的就是存放對象實例。
從內(nèi)存回收的角度來看枉证,由于現(xiàn)在收集器基本都采用分代收集算法矮男,所以Java堆中還可以細分為:新生代和老年代。從內(nèi)存分配的角度來看室谚,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(qū)(TLAB)毡鉴,進一步劃分的目的是為了更好地回收內(nèi)存崔泵,或者更快地分配內(nèi)存。
根據(jù)Java虛擬機規(guī)范規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可彪置。在實現(xiàn)時里伯,既可以實現(xiàn)成固定大小的,也可以是可擴展的崎弃,不過當前主流的虛擬機都是按照可擴展來實現(xiàn)的甘晤。如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時饲做,將會拋出OutOfMemoryError異常线婚。
方法區(qū)
方法區(qū)也是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息盆均、常量塞弊、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)泪姨。雖然Java虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分游沿,但是它卻有一個別名叫做Non-Heap(非堆),目的是與Java堆區(qū)分開來肮砾。
對于習(xí)慣在HotSpot虛擬機上開發(fā)和部署程序的開發(fā)者來說诀黍,很多人都更愿意把方法區(qū)稱為"永久代"(Permanent Generation),本質(zhì)上兩者并不等價仗处,僅僅是因為HotSpot虛擬機的設(shè)計團隊選擇把GC分代收集擴展至方法區(qū)眯勾,或者說使用永久代來實現(xiàn)方法區(qū)而已,這樣Hotspot的垃圾收集器可以像管理Java堆一樣管理這部分內(nèi)存婆誓,能夠省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作吃环。對于其他虛擬機(如BEA JRockit、IBMJ9等)來說是不存在永久代的概念的洋幻。使用永久代來實現(xiàn)方法區(qū)郁轻,現(xiàn)在看來并不是一個好主意,因為這樣更容易遇到內(nèi)存溢出問題鞋屈,而且有極少數(shù)方法(例如String.intern())會因這個原因?qū)е虏煌摂M機下有不同的表現(xiàn)范咨。所以在目前已經(jīng)發(fā)布的JDK1.7的HotSpot中,已經(jīng)把原本放在永久代的字符串常量池移出厂庇。
Java虛擬機規(guī)范對方法區(qū)的限制非常寬松渠啊,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴展外,還可以選擇不實現(xiàn)垃圾收集权旷。相對而言替蛉,垃圾收集行為在這個區(qū)域是比較少出現(xiàn)的贯溅,但并非數(shù)據(jù)進入了方法區(qū)就如永久代的名字一樣永久存在了。這區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載躲查。
根據(jù)Java虛擬機規(guī)范的規(guī)定它浅,當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常镣煮。
運行時常量池
運行時常量池是方法區(qū)的一部分姐霍。Class文件中除了有類的版本、字段典唇、方法镊折、接口等描述信息外,還有一項信息是常量池(叫做Class文件常量池)介衔,用于存放編譯期生成的各種字面量和符號引用恨胚,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放炎咖。
運行時常量池相對于Class文件常量池的另外一個重要特征是具備動態(tài)性升熊,Java語言并不要求常量一定只有編譯期才能產(chǎn)生僚碎,也就是并非預(yù)置入Class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池勺阐,運行期間也可能將新的常量放入池中渊抽,這種特性被開發(fā)人員利用的比較多的便是String類的intern()方法懒闷。JDK1.8版本中,String常量池已經(jīng)從方法區(qū)中的運行時常量池分離到堆中了玩焰。
既然運行時常量池是方法區(qū)的一部分芍锚,自然受到方法區(qū)內(nèi)存的限制蔓榄,當常量池無法再申請到內(nèi)存時會拋出OutOfMemoryError異常甥郑。