說到 Java 虛擬機(jī)(Java Virtual Machine, 簡稱 JVM)歉甚,可能對于我們大部分 Java 程序員來說都感覺望而生畏,都覺得它很高大上群扶,畢竟我們都知道因為它我們的 Java 程序才能做到一次編寫术辐,到處運(yùn)行渡处,而且因為它我們才能夠做到只專注于業(yè)務(wù)代碼實現(xiàn),而不用去關(guān)心內(nèi)存分配和回收的事情梦碗,僅從這兩點(diǎn)就能看出 Java 虛擬機(jī)為我們做了多少事情禽绪,但也正因為它為我們做的事情太多了,以至于我們只需要一心一意的去實現(xiàn)我們的需求洪规,在大多數(shù)情況下我們都不用去關(guān)心底層如何做到的印屁,但也因為它做了太多,導(dǎo)致我們在編碼過程中遇到一些問題時根本不知道發(fā)生了什么斩例,更別提如何解決了雄人,因此我們還是有必要去了解一下 Java 虛擬機(jī)到底是怎么為我們服務(wù)的,這樣在遇到問題時才不至于手足無措樱拴。
首先柠衍,讓我們對 JVM 有個初步的印象,知道它是什么晶乔,然后能做什么珍坊。JVM 在維基百科上的定義如下:
Java 虛擬機(jī)(Java Virtual Machine,JVM)正罢,一種能夠運(yùn)行 Java bytecode(字節(jié)碼) 的虛擬機(jī)阵漏,以堆棧結(jié)構(gòu)機(jī)器來進(jìn)行實現(xiàn)。最早由 Sun 微系統(tǒng)所研發(fā)并實現(xiàn)第一個實現(xiàn)版本翻具,是 Java 平臺的一部分履怯,能夠運(yùn)行以 Java 語言寫作的軟件程序。
Java 虛擬機(jī)有自己完善的硬體架構(gòu)裆泳,如處理器叹洲、堆棧、寄存器等工禾,還具有相應(yīng)的指令系統(tǒng)运提。JVM 屏蔽了與具體操作系統(tǒng)平臺相關(guān)的信息,使得 Java 程序只需生成在 Java 虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼)闻葵,就可以在多種平臺上不加修改地運(yùn)行民泵。
作為一種編程語言的虛擬機(jī),實際上不只是專用于 Java 語言槽畔,只要生成的編譯文件符合 JVM 對加載編譯文件格式要求栈妆,任何語言都可以由 JVM 編譯運(yùn)行。
以上是維基百科對 JVM 的有關(guān)定義,通俗一點(diǎn)來說就是我們編寫的 Java 程序經(jīng)過編譯之后得到的字節(jié)碼就可以放到 JVM 上去運(yùn)行了鳞尔,至于是怎么運(yùn)行的就和 JVM 內(nèi)部的實現(xiàn)有關(guān)了嬉橙,然后由于最終在 JVM 上運(yùn)行的是編譯之后的字節(jié)碼,所以它不僅僅是只能運(yùn)行 Java 程序寥假,只要編譯之后的文件符合 JVM 的要求就能夠運(yùn)行憎夷。
下面我們就從 JVM 的運(yùn)行時數(shù)據(jù)區(qū)域結(jié)構(gòu)說起,其實這也是我們經(jīng)趁林迹看到的一道面試題: Java 虛擬機(jī)的運(yùn)行時數(shù)據(jù)區(qū)域分布是怎樣的拾给?如果你準(zhǔn)備過面試,相信你肯定看到過這道面試題兔沃,我記得我剛畢業(yè)出來準(zhǔn)備面試的時候就經(jīng)辰茫看到這道面試題,而當(dāng)時說實話可能對 Java 虛擬機(jī)都沒什么概念乒疏,所以可想而知在準(zhǔn)備面試的時候只能是死記硬背答案了额衙,以至于過一段時間就忘了,而現(xiàn)在對 Java 虛擬機(jī)不敢說非常熟悉怕吴,但起碼也算是知道個一二了窍侧,下面就一起來看看回答這道題需要準(zhǔn)備哪些知識點(diǎn),看完這些知識點(diǎn)之后相信你也知道該怎么回答這道題了转绷。
JVM 定義了在程序執(zhí)行期間各種運(yùn)行時數(shù)據(jù)區(qū)域伟件,主要有堆,程序計數(shù)器议经,棧斧账,方法區(qū),運(yùn)行時常量池煞肾,其中一些數(shù)據(jù)區(qū)域隨著 JVM 的啟動而創(chuàng)建咧织,在 JVM 退出時銷毀,有一些則是線程私有的籍救,在線程創(chuàng)建時初始化习绢,線程退出時銷毀,每個區(qū)域承擔(dān)著不同的使命蝙昙,下面來簡單看下每個區(qū)域主要負(fù)責(zé)的內(nèi)容闪萄。
堆
所有的類實例以及數(shù)組在這里進(jìn)行內(nèi)存分配,在 JVM 啟動時創(chuàng)建耸黑,并且所有 JVM 線程共享桃煎,即該區(qū)域的類實例和數(shù)組所有線程都能夠訪問篮幢,該區(qū)域大小可以是固定的大刊,也可以根據(jù)需要彈性擴(kuò)展,JVM 提供了啟動參數(shù)供程序員指定堆的初始化大小,如果是彈性的需指定堆的最大值缺菌。JVM 指定堆內(nèi)存的初始化大小參數(shù)為 -Xms葫辐,配置堆內(nèi)存的最大值參數(shù)為 -Xmx。如果需要堆內(nèi)存大小固定伴郁,只需要將 -Xms 和 -Xmx 的值配置相同即可耿战,比如說 -Xms20m -Xmx20m。
由于我們的程序可能無時無刻不在創(chuàng)建對象和數(shù)組焊傅,而有的對象或數(shù)組在創(chuàng)建好用完一次可能再也不需要了剂陡,這時就需要垃圾收集器來進(jìn)行內(nèi)存回收,因此堆區(qū)相對來說是垃圾收集器重點(diǎn)關(guān)注的區(qū)域狐胎,當(dāng) JVM 在該區(qū)域進(jìn)行內(nèi)存分配時遇到所需要的堆內(nèi)存大于該區(qū)域可用的內(nèi)存時鸭栖,JVM 將會拋出 OutOfMemoryError 錯誤。
至于堆區(qū)中的對象實例是如何分配以及垃圾收集器又是如何回收的握巢,可能需要單獨(dú)用好幾篇文章來說明晕鹊,這個先知道有這回事就行,待下回分解暴浦,今天先重點(diǎn)關(guān)注 JVM 的運(yùn)行時內(nèi)存區(qū)域分布溅话。
程序計數(shù)器
JVM 支持一次多個線程的執(zhí)行,每個線程都擁有自己的程序計數(shù)器歌焦,在任意時刻飞几,每個線程當(dāng)前正在執(zhí)行的方法稱為該線程的當(dāng)前方法,如果當(dāng)前方法不是本地方法(Native Method),程序計數(shù)器中的值是當(dāng)前線程正在執(zhí)行的當(dāng)前方法中指令的地址独撇,如果是本地方法循狰,它的值是 undefined。在多線程環(huán)境中券勺,每個線程的調(diào)度執(zhí)行都是通過 CPU 來分配時間片的绪钥,當(dāng)一個線程分配的時間片執(zhí)行完之后需要重新回到就緒狀態(tài),等待下一次調(diào)度再恢復(fù)執(zhí)行关炼,而線程的恢復(fù)執(zhí)行能回到上次執(zhí)行的位置繼續(xù)執(zhí)行靠的就是程序計數(shù)器中記錄的值程腹。
棧
-
虛擬機(jī)棧
同樣地每個線程也都擁有一個自己的虛擬機(jī)棧,隨著線程的創(chuàng)建而創(chuàng)建儒拂,它里面主要存儲著方法執(zhí)行過程中產(chǎn)生的方法棧幀寸潦,棧幀中包含有局部變量表,操作數(shù)棧社痛,動態(tài)連接见转,方法的出口等信息,每一個方法從被調(diào)用到執(zhí)行返回的過程都對應(yīng)著一個棧幀在虛擬機(jī)棧中壓棧到彈棧的過程蒜哀。
在 JVM 規(guī)范中該區(qū)域大小可以是固定的斩箫,也可以根據(jù)需要彈性擴(kuò)展,JVM 提供了啟動參數(shù)供程序員指定虛擬機(jī)棧的初始化大小,如果是彈性的需指定最大值乘客。而且該區(qū)域可能有兩種異常產(chǎn)生狐血,一個是如果線程中請求的棧深度超過了 JVM 所允許的最大深度,將拋出 StackOVerflowError 錯誤易核,另一個是如果 JVM 的棧內(nèi)存支持動態(tài)擴(kuò)展的話匈织,當(dāng)棧在嘗試擴(kuò)展的過程中已經(jīng)沒有足夠的內(nèi)存來支持?jǐn)U展,或者在創(chuàng)建線程的時候就已經(jīng)沒有足夠的內(nèi)存來為新創(chuàng)建的線程初始化棧牡直,JVM 將拋出 OutOfMemoryError 錯誤缀匕。
而對于具體的 HotSpot 虛擬機(jī)來說,它是不支持?jǐn)U展的碰逸,在創(chuàng)建線程初始化棧內(nèi)存時就已經(jīng)確定大小了弦追,可通過 -Xss 參數(shù)指定棧容量的大小,比如說 -Xss20m 就是指定棧容量大小為 20MB花竞。因此在 HotSpot 虛擬機(jī)中是不存在線程運(yùn)行過程中由于棧的擴(kuò)展而產(chǎn)生 OutOfMemoryError 錯誤劲件,只可能在創(chuàng)建線程初始化棧內(nèi)存的時候就已經(jīng)無法初始化才會產(chǎn)生 OutOfMemoryError 錯誤。
-
本地方法棧
本地方法棧(Native Method Stacks)和虛擬機(jī)棧的作用其實是一樣的约急,只不過為了支持 Java 中的本地方法所以才有了本地方法棧的存在零远,相關(guān)特征可參考虛擬機(jī)棧中的內(nèi)容。
方法區(qū)
和堆區(qū)一樣同樣是在 JVM 啟動時就創(chuàng)建厌蔽,所有 JVM 線程共享牵辣,但該區(qū)域主要用于存儲加載完成的每個類的運(yùn)行時常量池,類型信息奴饮,常量纬向,靜態(tài)變量,字段和方法以及即時編譯器編譯后的代碼緩存等這些類的結(jié)構(gòu)戴卜。方法區(qū)邏輯上來說是堆區(qū)的一部分逾条,但有時為了將它與堆作區(qū)分從而叫它非堆。
JVM 規(guī)范中對方法區(qū)的規(guī)定其實是很寬泛的投剥,對于方法區(qū)的實現(xiàn)方式也就由具體的虛擬機(jī)自己去定義了师脂,在 JDK 8 以前,其中 HotSpot 虛擬機(jī)通過永久代(垃圾收集器的分代設(shè)計中的永久代)的方式來實現(xiàn)方法區(qū)江锨,這樣就可以直接采取垃圾收集器管理堆區(qū)的方式去管理方法區(qū)吃警,省去了為方法區(qū)設(shè)計內(nèi)存管理的工作。但是因為永久代的大小有個上限啄育,通過 -XX:MaxPermSize=size 設(shè)置酌心,即使不設(shè)置也會有個默認(rèn)值,這就導(dǎo)致了 JVM 更容易遇到內(nèi)存溢出的問題挑豌,因此在后來 JDK 發(fā)展的過程中開始逐步放棄永久代的實現(xiàn)方式安券,JDK 8 之后直接改成通過元空間(Metaspace)的方式實現(xiàn)墩崩,可通過 -XX:MaxMetaspaceSize=size 設(shè)置元空間最大值,默認(rèn)是不做限制的完疫,但由于元空間位于本地內(nèi)存,也就是僅僅受限于本地內(nèi)存的大小债蓝。
對于該區(qū)域在 JVM 規(guī)范中明確說明簡單的實現(xiàn)可以選擇不進(jìn)行垃圾收集或壓縮壳鹤,那是因為該區(qū)域存儲的是主要是類的結(jié)構(gòu)數(shù)據(jù),垃圾收集器回收的也就是類的信息饰迹,而對于類的回收芳誓,需要進(jìn)行類卸載,類卸載的條件又異常嚴(yán)格啊鸭,也就導(dǎo)致垃圾收集器在該區(qū)域的回收效果實在是差強(qiáng)人意锹淌,但不管如何還是有必要對該區(qū)域進(jìn)行回收,以防該區(qū)域產(chǎn)生內(nèi)存泄漏問題赠制。
盡管 JVM 規(guī)范中描述如果方法區(qū)中的內(nèi)存不能夠繼續(xù)滿足內(nèi)存分配請求的話將會拋出 OutOfMemoryError 錯誤赂摆,但是由于在 JDK 8 之后已經(jīng)改為通過元空間(Metaspace)的方式實現(xiàn)該區(qū)域,因此基本上不會在該區(qū)域產(chǎn)生 OutOfMemoryError 錯誤钟些,除非本地內(nèi)存嚴(yán)重不夠烟号,已經(jīng)無法為運(yùn)行時產(chǎn)生的類信息分配內(nèi)存了。
運(yùn)行時常量池
在方法區(qū)中其實也提到了運(yùn)行時常量池政恍,它是方法區(qū)中的一部分汪拥,主要存儲著編譯期間生成的各種字面量和符號引用,這些內(nèi)容在類加載后存放至該區(qū)域中篙耗。同樣的在 JVM 規(guī)范描述中迫筑,創(chuàng)建類或接口時,如果運(yùn)行時常量池構(gòu)造所需的內(nèi)存已經(jīng)超過 Java 虛擬機(jī)方法區(qū)中可用的內(nèi)存宗弯,則 Java 虛擬機(jī)將拋出 OutOfMemoryError 錯誤脯燃,但正如上面所說的原因已經(jīng)很難在該區(qū)域產(chǎn)生 OutOfMemoryError 錯誤了。
直接內(nèi)存
直接內(nèi)存不是 JVM 運(yùn)行時數(shù)據(jù)區(qū)域的一部分蒙保,在 JVM 的規(guī)范中也沒有定義該內(nèi)存區(qū)域曲伊,只不過由于這部分內(nèi)存也是經(jīng)常用到的,也可能導(dǎo)致 OutOfMemoryError 錯誤追他,可通過 -XX:MaxDirectMemorySize=size 參數(shù)來設(shè)置可使用的最大直接內(nèi)存坟募。其實這部分內(nèi)存我也不是非常懂,可能很少接觸到的原因邑狸,只是在《深入理解 Java 虛擬機(jī)》一書中看到說在 JDK 1.4 中加入的 NIO 中就利用到了這部分內(nèi)存懈糯,避免了由于使用 Native 方法在 Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)的開銷赁濒,顯著提高了性能瑰剃。
這部分內(nèi)存雖然說不會像其他區(qū)域一樣有大小限制,但最終還是會受到本機(jī)的總內(nèi)存大小的限制沉御,因此我們可能只記得設(shè)置堆的大小參數(shù),卻忘了還有這部分內(nèi)存的消耗屿储,導(dǎo)致各個區(qū)域的內(nèi)存加起來超過了本機(jī)的總內(nèi)存大小贿讹,從而產(chǎn)生 OutOfMemoryError 錯誤。
總結(jié)
上面主要介紹了 Java 虛擬機(jī)的運(yùn)行時數(shù)據(jù)區(qū)域分布够掠,以及每個區(qū)域所負(fù)責(zé)的內(nèi)容民褂,這里我強(qiáng)烈推薦周志明著的《深入理解Java虛擬機(jī)》這本書,書中對上面的每個知識點(diǎn)都有非常全面的解析疯潭,相信這本書看完之后你對 JVM 會有更加全面的認(rèn)識赊堪。這里先通過上面的內(nèi)容讓我們對 Java 虛擬機(jī)能夠有一個初步的了解,接下來我將對每個區(qū)域進(jìn)行擴(kuò)展竖哩,并且進(jìn)行深入了解哭廉,在這個深入了解的過程中再進(jìn)一步的熟悉 Java 虛擬機(jī),這可以幫助我們在遇到詭異的問題的時候知道是怎么產(chǎn)生的相叁,然后該怎么去解決遵绰,同時也會對我們的日常編碼有很好地指導(dǎo)作用,知其然而且知其所以然增淹。
微信公眾號:rookiedev街立,Java 后臺開發(fā),勵志終身學(xué)習(xí)埠通,堅持原創(chuàng)干貨輸出赎离,你可選擇現(xiàn)在就關(guān)注我,或者看看歷史文章再關(guān)注也不遲端辱。長按二維碼關(guān)注梁剔,我們一起努力變得更優(yōu)秀!