當(dāng)我們第一次學(xué)習(xí)Java時(shí)這些原理上的東西就會(huì)被提到,但是很少有真正去學(xué)習(xí)偷厦。今天開始從頭過一遍Java霹肝,打算從JVM開始申钩。
3.1. 程序計(jì)數(shù)器(PC, Program Counter)
3.2. Java虛擬機(jī)棧(Stack,Java Virtual Mechine Stacks)
3.4. Java 堆(Heap, Garbage Collection Heap)
3.8. 運(yùn)行時(shí)常量池(Run-Time Constant Pool)
1. JVM是什么
JVM是Java Virtual Mechine的縮寫痹升。它是一種基于計(jì)算設(shè)備的規(guī)范建炫,是一臺(tái)虛擬機(jī),即虛構(gòu)的計(jì)算機(jī)疼蛾。
JVM屏蔽了具體操作系統(tǒng)平臺(tái)的信息(顯然,就像是我們?cè)陔娔X上開了個(gè)虛擬機(jī)一樣)艺配,當(dāng)然察郁,JVM執(zhí)行字節(jié)碼時(shí)實(shí)際上還是要解釋成具體操作平臺(tái)的機(jī)器指令的。
通過JVM转唉,Java實(shí)現(xiàn)了平臺(tái)無關(guān)性皮钠,Java語言在不同平臺(tái)運(yùn)行時(shí)不需要重新編譯,只需要在該平臺(tái)上部署JVM就可以了赠法。因而能實(shí)現(xiàn)一次編譯多處運(yùn)行麦轰。(就像是你的虛擬機(jī)也可以在任何安了VMWare的系統(tǒng)上運(yùn)行)
2. JRE和JDK
JRE:Java Runtime Environment,也就是JVM的運(yùn)行平臺(tái)砖织,聯(lián)系平時(shí)用的虛擬機(jī)款侵,大概可以理解成JRE=虛擬機(jī)平臺(tái)+虛擬機(jī)本體(JVM)。類似于你電腦上的VMWare+適用于VMWare的Ubuntu虛擬機(jī)侧纯。這樣我們也就明白了JVM到底是個(gè)什么新锈。
JDK:Java Develop Kit,Java的開發(fā)工具包眶熬,JDK本體也是Java程序妹笆,因此運(yùn)行依賴于JRE,由于需要保持JDK的獨(dú)立性與完整性,JDK的安裝目錄下通常也附有JRE娜氏。目前Oracle提供的Windows下的JDK安裝工具會(huì)同時(shí)安裝一個(gè)正常的JRE和隸屬于JDK目錄下的JRE拳缠。
3. JVM結(jié)構(gòu)
JVM主要包括:程序計(jì)數(shù)器(Program Counter),Java堆(Heap)贸弥,Java虛擬機(jī)棧(Stack)窟坐,本地方法棧(Native Stack),方法區(qū)(Method Area)
詳細(xì)的結(jié)構(gòu)如下:
現(xiàn)在我來分別介紹一下每一部分的功能茂腥。
3.1. 程序計(jì)數(shù)器(PC, Program Counter)
是一個(gè)寄存器狸涌,可以看作是代碼行號(hào)指示器,類似于實(shí)際計(jì)算機(jī)里的PC最岗,用于指示帕胆,跳轉(zhuǎn)下一條需要執(zhí)行的命令。Java的基礎(chǔ)操作以及異常處理等都十分依賴PC般渡。
JVM多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的懒豹。在一個(gè)確定的時(shí)刻芙盘,一個(gè)處理器(或者說多核處理器的一個(gè)內(nèi)核)只會(huì)執(zhí)行一條線程中的命令。因此脸秽,為了正常的切換線程儒老,每個(gè)線程都會(huì)有一個(gè)獨(dú)立的PC,各線程的PC不會(huì)互相影響记餐。這個(gè)私有的PC所占的這塊內(nèi)存即是線程的“私有內(nèi)存”驮樊。
如果線程在執(zhí)行的是Java方法,那么PC記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址片酝。如果正在執(zhí)行的不是Java方法即Native方法囚衔,那么PC的值為undefined。
PC的內(nèi)存區(qū)域是唯一的沒有規(guī)定任何OutOfMemoryError的Java虛擬機(jī)規(guī)范中的區(qū)域雕沿。
3.2. Java虛擬機(jī)棧(Stack练湿,Java Virtual Mechine Stacks)
同PC一樣(從工作流程圖里我們可以看到,實(shí)際上审轮,PC也是存在于JVM Stack上的)肥哎,也是線程私有的,生命周期與線程相同疾渣。虛擬機(jī)棧描述Java方法執(zhí)行的內(nèi)存模型篡诽,每個(gè)方法被執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),棧幀會(huì)利用局部變量數(shù)組存儲(chǔ)局部變量(Local Variables)稳衬,操作棧(Operand Stack)霞捡,方法出口(Return Value),動(dòng)態(tài)連接(Current Class Constant Pool Reference)等信息薄疚。
局部變量數(shù)組存儲(chǔ)了編譯可知的八個(gè)基本類型(int, boolean, char, short, byte, long, float, double)碧信,對(duì)象引用(根據(jù)不同的虛擬機(jī)實(shí)現(xiàn)可能是引用地址的指針或者一個(gè)handle),returnAddress類型街夭。64位的long和double會(huì)占用兩個(gè)Slot砰碴,其余類型會(huì)占用一個(gè)Slot。在編譯期間板丽,局部變量所需的空間就會(huì)完成分配呈枉,動(dòng)態(tài)運(yùn)行期間不會(huì)改變所需的空間。
操作棧在執(zhí)行字節(jié)碼指令時(shí)會(huì)被用到埃碱,這種方式類似于原生的CPU寄存器猖辫,大部分JVM把時(shí)間花費(fèi)在操作棧的花費(fèi)上,操作棧和局部變量數(shù)組會(huì)頻繁的交換數(shù)據(jù)砚殿。
動(dòng)態(tài)連接控制著運(yùn)行時(shí)常量池和棧幀的連接啃憎。所有方法和類的引用都會(huì)被當(dāng)作符號(hào)的引用存在常量池中。符號(hào)引用是實(shí)際上并不指向物理內(nèi)存地址的邏輯引用似炎。JVM 可以選擇符號(hào)引用解析的時(shí)機(jī)辛萍,一種是當(dāng)類文件加載并校驗(yàn)通過后悯姊,這種解析方式被稱為饑餓方式。另外一種是符號(hào)引用在第一次使用的時(shí)候被解析贩毕,這種解析方式稱為惰性方式悯许。無論如何 ,JVM 必須要在第一次使用符號(hào)引用時(shí)完成解析并拋出可能發(fā)生的解析錯(cuò)誤辉阶。綁定是將對(duì)象域先壕、方法、類的符號(hào)引用替換為直接引用的過程睛藻。綁定只會(huì)發(fā)生一次启上。一旦綁定,符號(hào)引用會(huì)被完全替換店印。如果一個(gè)類的符號(hào)引用還沒有被解析,那么就會(huì)載入這個(gè)類倒慧。每個(gè)直接引用都被存儲(chǔ)為相對(duì)于存儲(chǔ)結(jié)構(gòu)(與運(yùn)行時(shí)變量或方法的位置相關(guān)聯(lián)的)偏移量按摘。
對(duì)Java虛擬機(jī)棧這個(gè)區(qū)域,Java虛擬機(jī)規(guī)范規(guī)定了兩種異常:
線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度纫谅,拋出StackOverFlow異常炫贤。
對(duì)于支持動(dòng)態(tài)擴(kuò)展的虛擬機(jī),當(dāng)擴(kuò)展無法申請(qǐng)到足夠的內(nèi)存時(shí)會(huì)拋出OutOfMemory異常付秕。
3.3. 本地方法棧(Native Stack)
本地方法棧如其名字兰珍,和Java Virtual Machine Stack其實(shí)極為類似,只是執(zhí)行的是Native方法询吴,為Native方法服務(wù)掠河。在JVM規(guī)范中,沒有對(duì)它的實(shí)現(xiàn)做具體規(guī)定猛计。
3.4. Java 堆(Heap, Garbage Collection Heap)
Java堆是被所有線程共享的一塊區(qū)域唠摹,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例奉瘤,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存(隨著技術(shù)的發(fā)展勾拉,已不絕對(duì))。
Java堆是垃圾收集器管理的主要區(qū)域盗温,因而也被稱為GC堆藕赞。收集器采用分代回收法,GC堆可以分為新生代(Yong Generation)和老生代(Old Generation)卖局。新生代包括Eden Space和Survivor Space斧蜕。但無論哪個(gè)區(qū)域,如何劃分吼驶,存儲(chǔ)的都是Java對(duì)象實(shí)例惩激,進(jìn)一步的劃分是為了更好的回收內(nèi)存或快速的分配內(nèi)存店煞。
根據(jù)Java虛擬機(jī)規(guī)范,堆所在的物理內(nèi)存區(qū)間可以是不連續(xù)的风钻,只要邏輯連續(xù)就可以顷蟀。實(shí)現(xiàn)時(shí)既可以是固定大小,也可以是可擴(kuò)展的骡技。如果堆無法擴(kuò)展時(shí)鸣个,就會(huì)拋出OutOfMemoryError。
3.5. 方法區(qū)(Method Area)
方法區(qū)和Java堆類似布朦,也屬于各線程共享的內(nèi)存區(qū)域囤萤。用于存儲(chǔ)已被虛擬機(jī)加載的類信息,常量是趴,靜態(tài)變量涛舍,即時(shí)編譯器編譯后的代碼數(shù)據(jù)等。它屬于非堆區(qū)(Non Heap)唆途,和Java堆區(qū)分開富雅。對(duì)于存在永久代(Permanent)概念的虛擬機(jī)(HotSpot)而言,方法區(qū)存在于永久代肛搬。Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的規(guī)定很寬松没佑,甚至可以不實(shí)現(xiàn)GC。不過并非進(jìn)入方法區(qū)的數(shù)據(jù)就會(huì)永久存在了温赔,這塊區(qū)域的內(nèi)存回收主要為常量池的回收和類型的卸載蛤奢。這個(gè)區(qū)域的回收處理不善也會(huì)導(dǎo)致嚴(yán)重的內(nèi)存泄漏。
當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)也會(huì)拋出OutOfMemoryError陶贼。
3.6. 代碼緩存(Code Cache)
用于編譯和存儲(chǔ)那些被 JIT 編譯器編譯成原生代碼的方法啤贩。
3.7. 類信息(Class Data)
類信息存儲(chǔ)在方法區(qū),其主要構(gòu)成為運(yùn)行時(shí)常量池(Run-Time Constant Pool)和方法(Method Code)骇窍。
一個(gè)編譯后的類文件包括以下結(jié)構(gòu):
結(jié)構(gòu)解釋
magic, minor_version, major_version類文件的版本信息和用于編譯這個(gè)類的 JDK 版本瓜晤。
constant_pool類似于符號(hào)表,盡管它包含更多數(shù)據(jù)腹纳。下面有更多的詳細(xì)描述痢掠。
access_flags提供這個(gè)類的描述符列表。
this_class提供這個(gè)類全名的常量池(constant_pool)索引嘲恍,比如org/jamesdbloom/foo/Bar足画。
super_class提供這個(gè)類的父類符號(hào)引用的常量池索引。
interfaces指向常量池的索引數(shù)組佃牛,提供那些被實(shí)現(xiàn)的接口的符號(hào)引用淹辞。
fields提供每個(gè)字段完整描述的常量池索引數(shù)組。
methods指向constant_pool的索引數(shù)組俘侠,用于表示每個(gè)方法簽名的完整描述象缀。如果這個(gè)方法不是抽象方法也不是 native 方法蔬将,那么就會(huì)顯示這個(gè)函數(shù)的字節(jié)碼。
attributes不同值的數(shù)組央星,表示這個(gè)類的附加信息霞怀,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解。
3.8. 運(yùn)行時(shí)常量池(Run-Time Constant Pool)
運(yùn)行時(shí)常量池是方法區(qū)的一部分莉给。Class文件中有類的版本毙石,字段,方法颓遏,接口等描述信息和用于存放編譯期生成的各種字面量和符號(hào)引用徐矩。這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。Java虛擬機(jī)規(guī)范對(duì)Class的細(xì)節(jié)有著嚴(yán)苛的要求而對(duì)運(yùn)行時(shí)常量池的實(shí)現(xiàn)不做要求叁幢。一般來說除了翻譯的Class,翻譯出來的直接引用也會(huì)存在運(yùn)行時(shí)常量池中滤灯。
運(yùn)行時(shí)常量池具備動(dòng)態(tài)性,即運(yùn)行時(shí)也可將新的常量放入池中曼玩。比如String類的intern()方法力喷。
常量池?zé)o法申請(qǐng)到足夠的內(nèi)存分配時(shí)也會(huì)拋出OutOfMemoryError。
3.9. 直接內(nèi)存(Direct Memory)
直接內(nèi)存并不在Java虛擬機(jī)規(guī)范中演训,不是Java的一部分,但是也被頻繁使用并可能導(dǎo)致OutOfMemoryError贝咙。Native函數(shù)庫可以直接分配堆外內(nèi)存样悟,通過存儲(chǔ)在Java堆里的DirectDataBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣做在一些場(chǎng)景中可以顯著提高性能庭猩。
直接內(nèi)存是堆外內(nèi)存窟她,自然不受Java堆大小的限制,但是可能受實(shí)體機(jī)內(nèi)存大小的限制蔼水。如果內(nèi)存各部分總和大于實(shí)體機(jī)的內(nèi)存時(shí)震糖,也會(huì)報(bào)出OutOfMemoryError。
4. Java垃圾回收
將內(nèi)存中不再被使用的對(duì)象進(jìn)行回收趴腋,GC中用于回收的方法稱為收集器吊说,由于GC需要消耗一些資源和時(shí)間,Java在對(duì)對(duì)象的生命周期特征進(jìn)行分析后优炬,按照新生代颁井、舊生代的方式來對(duì)對(duì)象進(jìn)行收集,以盡可能的縮短GC對(duì)應(yīng)用造成的暫停蠢护。
不同的對(duì)象引用類型雅宾, GC會(huì)采用不同的方法進(jìn)行回收,JVM對(duì)象的引用分為了四種類型:
強(qiáng)引用:默認(rèn)情況下葵硕,對(duì)象采用的均為強(qiáng)引用(這個(gè)對(duì)象的實(shí)例沒有其他對(duì)象引用眉抬,GC時(shí)才會(huì)被回收)贯吓。
軟引用:軟引用是Java中提供的一種比較適合于緩存場(chǎng)景的應(yīng)用(只有在內(nèi)存不夠用的情況下才會(huì)被GC)。
弱引用:在GC時(shí)一定會(huì)被GC回收蜀变。
虛引用:由于虛引用只是用來得知對(duì)象是否被GC悄谐。
5. JVM線程與原生線程的關(guān)系
JVM允許一個(gè)程序使用多個(gè)并發(fā)線程,Hotspot JVM中Java的線程與原生操作系統(tǒng)的線程是直接映射關(guān)系昏苏。即當(dāng)線程本地存儲(chǔ)尊沸、緩沖區(qū)分配、同步對(duì)象贤惯、棧洼专、程序計(jì)數(shù)器等準(zhǔn)備好以后,就會(huì)創(chuàng)建一個(gè)操作系統(tǒng)原生線程孵构。Java 線程結(jié)束屁商,原生線程隨之被回收。操作系統(tǒng)負(fù)責(zé)調(diào)度所有線程颈墅,并把它們分配到任何可用的 CPU 上蜡镶。當(dāng)原生線程初始化完畢,就會(huì)調(diào)用 Java 線程的 run() 方法恤筛。run() 返回時(shí)官还,被處理未捕獲異常,原生線程將確認(rèn)由于它的結(jié)束是否要終止 JVM 進(jìn)程(比如這個(gè)線程是最后一個(gè)非守護(hù)線程)毒坛。當(dāng)線程結(jié)束時(shí)望伦,會(huì)釋放原生線程和 Java 線程的所有資源。
6. 參考文章