一、簡介
????在上一篇文章《淺談Java虛擬機(一)—什么是Java虛擬機》中秕铛,我們已經(jīng)簡單了解Java虛擬機是什么,這篇文章我們來一起學習Java虛擬機的運行時數(shù)據(jù)區(qū)域但狭。
????Java程序員都知道攘烛,Java虛擬機擁有自己的內(nèi)存管理機制,在此機制下评抚,我們不再需要像C++程序員那樣豹缀,為每一個創(chuàng)建的對象手動刪除并釋放它所占用的內(nèi)存,凡事都有兩面性慨代,這個機制同時帶給我們的弊端就是如果不了解Java虛擬機邢笙,那么它就成為了一個黑匣子,我們不知道我們創(chuàng)建的對象存放在哪侍匙,是否存活氮惯,使用完后何時被清理,發(fā)生堆棧溢出時更無從查起...
? ? 因此想暗,了解Java虛擬機是如何使用內(nèi)存妇汗,如何劃分區(qū)域以及每個區(qū)域的作用是非常有必要的。
二说莫、運行時數(shù)據(jù)區(qū)域
????上圖為Java虛擬機規(guī)范所規(guī)定的運行時數(shù)據(jù)區(qū)域杨箭,顧名思義,這是一個規(guī)范储狭,相當于接口互婿,不同的虛擬機可以對其中每一個區(qū)域有自己的實現(xiàn)方式。
????圖中除了運行時數(shù)據(jù)區(qū)域辽狈,還展示了從加載字節(jié)碼文件到調(diào)用本地方法的基本流程慈参。首先,由類加載器將字節(jié)碼(.class)文件加載填充到運行時數(shù)據(jù)區(qū)刮萌,運行時數(shù)據(jù)區(qū)為進來的操作指令或數(shù)據(jù)使用對應的區(qū)域分配內(nèi)存驮配,執(zhí)行引擎不斷得從運行時數(shù)據(jù)區(qū)讀取指令,現(xiàn)在的Java字節(jié)碼機器是讀不懂的,因此還必須將字節(jié)碼轉(zhuǎn)化成平臺相關(guān)的機器碼(也就是系統(tǒng)能識別的0和1)壮锻,這個過程可以由解釋器來執(zhí)行琐旁,也可以由即時編譯器(JIT Compiler)來完成,最后通過本地庫接口調(diào)用本機操作系統(tǒng)的本地方法庫躯保,這樣就完成了從Java代碼到系統(tǒng)執(zhí)行的全過程旋膳。
? ? GC也存在于執(zhí)行引擎中。
????下面途事,將重點詳細介紹運行時數(shù)據(jù)區(qū)中各區(qū)域的作用和功能验懊。
????2.1 程序計數(shù)器
? ? ? ? 程序計數(shù)器是一塊較小的內(nèi)存空間,且空間大小不會隨著程序執(zhí)行而改變尸变,所以該區(qū)域是Java運行時內(nèi)存區(qū)域中唯一一個Java虛擬機規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域义图。
? ? ? ? 它可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器,例如召烂,執(zhí)行引擎中的字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令(僅在虛擬機概念模型中碱工,上面也提到,不同虛擬機對于不同區(qū)域有不同實現(xiàn)奏夫,可能某些虛擬機對于程序計數(shù)器的實現(xiàn)和使用有更高效的方式)怕篷,另外,代碼中的分支酗昼、循環(huán)廊谓、跳轉(zhuǎn)、異常處理麻削、線程恢復等都需要依賴程序計數(shù)器來完成蒸痹。
? ? ? ? 由于在程序計數(shù)器需要在多線程中完成線程恢復的功能,即在CPU切片后線程重新執(zhí)行時恢復到切片前的正確位置呛哟,因此叠荠,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間互不影響扫责,所以榛鼎,程序計數(shù)器是線程私有內(nèi)存區(qū)域,隨著線程的生命周期誕生和消亡鳖孤。
????2.2 Java虛擬機棧
????????Java虛擬機棧與程序計數(shù)器一樣借帘,也是線程私有的。
? ??????Java虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型淌铐,每個方法被執(zhí)行的同時都會創(chuàng)建一個棧幀,用于存儲局部變量表蔫缸、操作數(shù)棧腿准、動態(tài)鏈接、方法出口等信息,每一個方法被調(diào)用直至執(zhí)行完成的過程吐葱,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程街望。
????????Java虛擬機棧可以大致看作數(shù)據(jù)結(jié)構(gòu)中的棧弟跑,遵循后進先出(LIFO)的原則灾前,在方法嵌套的調(diào)用關(guān)系中,最先調(diào)用的方法最后執(zhí)行完成孟辑。從執(zhí)行引擎方面來描述哎甲,在活動線程中,只有位于棧頂?shù)臈攀怯行У乃撬裕Q為當前棧幀炭玫,與這個棧幀相關(guān)聯(lián)的方法稱為當前方法,執(zhí)行引擎運行的所有字節(jié)碼指令都只針對當前棧幀進行操作貌虾。
????????棧幀可以看作一個Java中的一個對象吞加,一個包裝了方法具體信息的特殊對象(其實不是,這樣說只是為了幫助理解棧幀是什么尽狠,它的實質(zhì)是描述方法的內(nèi)存模型)衔憨,其中:
????????局部變量表:變量值的存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量袄膏,這些變量類型有基本數(shù)據(jù)類型boolean践图、byte、char哩陕、short平项、int、float(以上數(shù)據(jù)類型為32位長度悍及,在32位虛擬機中剛好占用一個單位的局部變量空間闽瓢,局部變量空間中的最小單位被稱為變量槽Variable Slot,一般簡稱 Slot心赶,了解即可)扣讼、double、long(這兩個數(shù)據(jù)類型為明確定義的64位長度缨叫,在32位虛擬機中會占用兩個Slot)椭符,對象引用(即reference 類型,一個指向真實對象存儲地址的指針耻姥,在Java 虛擬機規(guī)范中沒有明確規(guī)定 reference 類型的長度销钝,可能是32位也可以是64位),以及retrunAddress類型(32位長度琐簇,請不要把這個直譯為返回地址蒸健,它并不是方法出口信息座享,切莫搞混,這個數(shù)據(jù)類型目前已經(jīng)很少見了似忧,它是為字節(jié)碼指令 jsr渣叛、jsr_w 和 ret 服務(wù)的,指向了一條字節(jié)碼指令的地址盯捌,以前的虛擬機會用這個實現(xiàn)異常處理淳衙,了解即可)。這里饺著,延伸一下箫攀,上面提到,虛擬機棧中的棧幀會被執(zhí)行引擎依次彈出執(zhí)行瓶籽,那么匠童,存在于棧幀中局部變量表的對象或引用指向的對象必然是存活的,這也是GC算法中的Root搜索算法(也稱作可達性分析算法)中為何會以局部變量表中的對象作為Root節(jié)點之一的原因塑顺,另外汤求,由于棧幀的彈出,棧中的變量等所占用的內(nèi)存也會被自動釋放严拒,這也致使虛擬機棧不是GC所關(guān)注的區(qū)域扬绪,更多詳細的會在后面針對Java虛擬機垃圾回收算法章節(jié)中介紹。
? ??????操作數(shù)棧:顧名思義裤唠,用于執(zhí)行方法時進行數(shù)據(jù)操作的棧結(jié)構(gòu)挤牛。在做算術(shù)運算或者調(diào)用其他方法的時候是通過操作數(shù)棧來進行參數(shù)傳遞的,例如种蘸,執(zhí)行引擎執(zhí)行到整數(shù)加法的字節(jié)碼指令時墓赴,發(fā)現(xiàn)操作數(shù)棧中最接近棧頂?shù)膬蓚€元素已經(jīng)存入了兩個整型的數(shù)值,會將這兩個整型值出棧并相加航瞭,然后將相加的結(jié)果入棧诫硕。
????????動態(tài)鏈接:因為在方法執(zhí)行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量池刊侯,通過運行時常量池的符號引用(指向堆)章办,完成將符號引用轉(zhuǎn)化為直接引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接滨彻。Class文件的常量池中存在有大量的符號引用藕届,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)。這些符號引用亭饵,一部分會在類加載階段或第一次使用的時候轉(zhuǎn)化為直接引用(如final休偶、static域等),稱為靜態(tài)解析辜羊,另一部分將在每一次的運行期間(例如方法執(zhí)行中)轉(zhuǎn)化為直接引用椅贱,這部分稱為動態(tài)連接懂算。
????????方法出口:無論是正常執(zhí)行完成還是拋出異常,方法返回后庇麦,需要返回到方法被調(diào)用的位置,因此棧幀中保存了返回位置信息喜德。
? ? ? ? Java虛擬機椛介希可以是固定長度也可以是動態(tài)擴展大小,要看具體虛擬機的實現(xiàn)(目前大部分虛擬機都可以動態(tài)擴展舍悯,同時也支持固定長度)航棱。當虛擬機棧為固定長度時,線程請求的深度大于虛擬機所允許的深度萌衬,就會拋出StackOverflowError饮醇,也就是常說的爆棧,結(jié)合上圖的虛擬機棧和棧幀的結(jié)構(gòu)秕豫,很容易想到的一種情況就是朴艰,當發(fā)生無限嵌套方法調(diào)用時,例如沒有邊界或者邊界很難達到的遞歸調(diào)用混移,棧幀就會無限入棧最終導致StackOverflowError祠墅;當虛擬機為動態(tài)擴展時,擴展過程中無法向虛擬機申請到足夠的內(nèi)存歌径,就會拋出OutOfMemoryError毁嗦,也就是內(nèi)存溢出。
? ? 2.3 本地方法棧
? ? ? ? 本地方法棧與虛擬機棧所發(fā)揮的作用是非常相似的回铛,它們之間的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法服務(wù)狗准,而本地方法棧則為虛擬機使用到的Native方法服務(wù),例如一些c文件茵肃。
? ? ? ? 執(zhí)行引擎在執(zhí)行指令過程中加載的本地方法就會壓入本地方法棧中腔长。
????????在Sun HotSpot的實現(xiàn)中,虛擬機棧與本地方法棧合二為一免姿。
????????本地方法棧也會拋出StackOverflowError和OutOfMemoryError
? ? 2.4 Java堆
? ? ? ? Java堆是Java虛擬機所管理的內(nèi)存中最大的一塊饼酿,此內(nèi)存區(qū)域的唯一目的就是存放對象實例。
? ? ? ? 在Java虛擬機規(guī)范描述中胚膊,所有的對象實例和數(shù)組都要在堆中分配內(nèi)存空間故俐,但隨著技術(shù)的發(fā)展,如今的Java虛擬機實現(xiàn)做出了大量技術(shù)優(yōu)化紊婉,使得這一描述不再絕對药版。僅管不再是“所有”, 但是絕大多數(shù)對象實例和數(shù)組仍然還是在堆中分配喻犁。
? ? ? ? 由于堆中的對象所存放的地址就是各種虛擬機棧棧幀中引用類型所指向的目標地址槽片,因此何缓,Java堆是所有線程共享的內(nèi)存區(qū)域。
????????當方法執(zhí)行完畢还栓,棧幀彈出碌廓,引用消失,但對象仍然在堆中剩盒,?若再無其他引用指向該對象谷婆,那么這個對象不再有用,或者稱為不再存活辽聊,這樣的對象如果大量存在卻沒有機制來清理纪挎,堆中沒有內(nèi)存完成新的實例分配,且同時也無法再動態(tài)擴展時跟匆,就會拋出OutOfMemoryError異常异袄。幸好,Java虛擬機有自己的清理機制玛臂,稱為垃圾收集器烤蜕,上述中的無用對象就會被垃圾收集器處理掉并釋放出占用的內(nèi)存,因此垢揩,Java堆是垃圾收集器所管理的主要區(qū)域玖绿,Java堆又被稱為GC堆(Garbage Collected Heap),但是叁巨,即使擁有這樣的機制斑匪,如果一次垃圾清理后搭幻,所有對象都被判斷為有用踪旷,新的實例進來仍然沒有空間分配,那么還是會拋出OutOfMemoryError異常绢掰。
? ? ? ? 由于當前垃圾回收器都是使用的分代收集算法庶橱,所以Java堆還可以分為:新生代和老年代贮勃,而新生代又可以分為 Eden 空間、From Survivor 空間苏章、To Survivor空間寂嘉,這樣的分法是服務(wù)于垃圾回收機制的,并不是Java堆本身枫绅,關(guān)于垃圾回收泉孩,會在后續(xù)文章中詳細介紹。
? ? 2.5 方法區(qū)
? ? ? ? 方法區(qū)與Java堆一樣并淋,是各個線程共享的內(nèi)存區(qū)域寓搬,它用于存儲已被虛擬機加載的類信息、常量(final static)县耽、靜態(tài)變量(static)句喷、JIT編譯后的代碼等數(shù)據(jù)镣典。
? ??????如何在堆中創(chuàng)建對象?就是從方法區(qū)中取的Class模板唾琼。
? ? ? ? 對于方法區(qū)兄春,定義很簡單,就上面這一句話父叙,但卻存在很多誤區(qū)神郊,這里將一一闡釋。
? ? ? ? 首先趾唱,再次強調(diào),方法區(qū)是Java虛擬機規(guī)范中定義的一個“接口”蜻懦,具體的實現(xiàn)與不同的虛擬機有關(guān)甜癞,這里還是以主流虛擬機Sun HotSpot為討論目標,在JDK1.7以前宛乃,Sun HotSpot對方法區(qū)的實現(xiàn)為永久代(PermGen)悠咱,永久代是上面提到過的GC分代收集的設(shè)計方案擴展,設(shè)計團隊用永久代來實現(xiàn)方法區(qū)就是為了讓GC在工作時也能照顧到方法區(qū)征炼,這樣就不用了專門為方法區(qū)設(shè)計一套內(nèi)存管理機制(取名永久代也很顧名思義析既,方法區(qū)中存在的內(nèi)容大多數(shù)都是“永久”存在的,例如常量谆奥、類信息)眼坏。然而,永久代的設(shè)計經(jīng)常遇到內(nèi)存溢出(OutOfMemoryError)的問題(原因也很容易想到酸些,GC在方法區(qū)清理本身就是一種消耗宰译,再加上方法區(qū)中沒有太多可清理的內(nèi)存),因此魄懂,設(shè)計團隊開始逐步放棄永久代沿侈,在JDK1.8中,使用元空間(Metaspace)來代替永久代實現(xiàn)方法區(qū)市栗,元空間并不在虛擬機中缀拭,而是使用本地內(nèi)存,理論上填帽,元空間的大小僅受本地內(nèi)存限制蛛淋,用戶可以為元空間設(shè)置一個可用空間最大值,如果不進行設(shè)置盲赊,JVM會自動根據(jù)類的元數(shù)據(jù)大小動態(tài)增加元空間的容量铣鹏,元空間獨立于Java虛擬機之外,所以其內(nèi)存管理由元空間虛擬機管理哀蘑。最后要提的一點是诚卸,元空間對方法區(qū)的實現(xiàn)并不代表元空間就沒有OutOfMemoryError異常葵第,OutOfMemoryError異常是Java虛擬機規(guī)范對方法區(qū)的規(guī)定,即當方法區(qū)無法滿足內(nèi)存分配需求時合溺,就必須拋出該異常卒密,元空間既然是方法區(qū)的實現(xiàn),當然也會遵循棠赛。
? ? ? ? 上面解釋了網(wǎng)上一些關(guān)于“元空間取代了方法區(qū)”這一錯誤說法哮奇,接下來介紹關(guān)于方法區(qū)另一個誤區(qū)較多的區(qū)域:運行時常量池。
? ? ? ? 從圖中可以看出睛约,運行時常量池是方法區(qū)的一部分鼎俘,經(jīng)常容易跟它混淆的是類常量池和字符串常量池。
????????這里另外再額外延伸提一個:對象池辩涝,基本類型的包裝類有對象池(也有稱常量池)的概念贸伐,用來緩存被創(chuàng)建出來的值,避免重復創(chuàng)建對象怔揩,方便以后使用捉邢,它和方法區(qū)中的類常量池,運行時常量池沒有任何關(guān)系商膊。類常量池和運行時常量池有點類似于符號表的概念伏伐,包裝類的對象池是池化技術(shù)的應用,并非是JVM層面的東西晕拆,而是 Java 在類封裝里實現(xiàn)的藐翎,包裝類的對象池的作用和字符串常量池類似,但字符串常量池卻是JVM層面的技術(shù)潦匈。
? ? ? ? JVM啟動后阱高,類加載器(ClassLoader)會把編譯好的.class文件經(jīng)過一系列加載過程(加載→驗證→準備→解析→初始化)將類的字節(jié)碼信息加載進方法區(qū),這里面除了一些描述信息外(例如茬缩,字段信息:修飾符赤惊、字段類型、字段名凰锡,也就是類似private String name()這段描述信息的提任粗邸),還有一項就是常量池(不管是描述信息還是類常量池都是編譯期形成的掂为,類加載器只是將他們加載進方法區(qū)而已)裕膀。
????????類常量池中包含了字面量和符號引用:
? ??????????????字面量:包含文本字符串、基本數(shù)據(jù)類型勇哗、聲明為final的常量等昼扛。
? ??????????????符號引用:顧名思義,是一種用來描述引用的符號, 符號可以是上述任何一種形式的字面量抄谐, 只要使用時能夠無歧義的定位到目標即可渺鹦,包含類的描述符號、方法的描述符號蛹含、字段的描述符號(不要與上面的描述信息搞混毅厚,例如,對于方法而言浦箱,描述信息是方法的標簽吸耿,即描述它的名字,訪問權(quán)限酷窥,返回類型等咽安,而描述符號是方法的在內(nèi)存中的位置,即引用蓬推,這個是在編譯期無法確定的板乙,才會用符號來代替)。
? ??????類常量池會在類加載過程中的解析階段被放入運行時常量池拳氢,方便程序運行時使用,其中的符號引用除了直接被保存進運行時常量池蛋铆,還會在這個階段被翻譯成直接引用存入(因為類信息已經(jīng)被加載到內(nèi)存中馋评,可以知道它的位置,程序運行時就可以從運行時常量池中拿到直接引用找到類刺啦、方法留特、字段)。
????????而類常量池字面量中的文本字符串就會在此階段進入運行時常量池中的字符串常量池玛瘸,只是字符串常量池在前面提到過的“永久代優(yōu)化”過程中蜕青,于JDK1.7正式從運行時常量池中移除,并放入了Java堆中糊渊,其余的東西仍在在運行時常量池中右核,所以,在JDK1.7以后渺绒,類常量池字面量中的文本字符串就會直接進入堆中的字符串常量池贺喝,字符串常量池位置的改變也導致了String.intern()這個方法在JDK7前后在使用的結(jié)果上出現(xiàn)了一些區(qū)別:
String s1=new String("a");
s1.intern();
String s2="a";
System.out.println(s1==s2);
運行結(jié)果:
JDK1.6運行結(jié)果:false
JDK1.7運行結(jié)果:true
? ? ? ? 要講清楚上面這段代碼,首先要逐步明白以下幾點:
? ? ? ? 1.String.intern()的作用:首先去判斷該字符串是否在字符串常量池中存在宗兼,如果存在返回字符串常量池中的字符串引用躏鱼,如果在字符串常量池中不存在,先在字符串常量池中添加該字符串殷绍,然后返回引用地址染苛。
? ? ? ? 2.從第1條中可以看出,字符串常量池中跟Java堆一樣也會存儲對象主到,不同的是只存字符串對象茶行。
? ? ? ? 3.new String("a")操作只會在Java堆中產(chǎn)生一個"a"字符串對象躯概,不會進入字符串常量池。
? ? ? ? 4.一個經(jīng)典問題:“String s1=new String("a");s1.intern();”拢军,這兩句代碼在內(nèi)存中產(chǎn)生幾個對象楞陷?答案是在JDK1.6中是2個,在JDK1.7中是1個茉唉。第一句代碼String s1=new String("a")會在堆中產(chǎn)生1個對象固蛾,這個毋庸置疑,結(jié)合第3條度陆,此時常量池中沒有"a"字符串對象艾凯,第二句代碼s1.intern()調(diào)用時,結(jié)合第1條和第2條懂傀,檢查到字符串常量池中沒有與"a"字符串equal()相等的對象趾诗,則會在字符串常量池中創(chuàng)建該對象,于是蹬蚁,在JDK1.6中恃泪,就會產(chǎn)生2個對象。而在JDK1.7中犀斋,由于字符串常量池搬到了堆中贝乎,很方便就能拿到對象的引用地址,于是叽粹,檢查到字符串常量池中沒有與"a"字符串equal()相等的對象時览效,不會在字符串常量池中創(chuàng)建一個新對象,而是把指向堆中"a"字符串對象的地址存入虫几,因此锤灿,在JDK1.7中,只會產(chǎn)生1個對象辆脸。
? ? ? ? 5.總結(jié)但校,JDK1.7前后對于前三點并無變化,重點在于第1條中String.intern()返回的引用地址發(fā)生了變化每强,可以看出始腾,在JDK1.7以前只會返回字符串常量池中的對象地址,而在JDK1.7以后空执,可能返回字符串常量池中的對象地址也可能返回堆中對象的地址浪箭,取決于誰先創(chuàng)建。例如辨绊,先寫String s1="a";s1.intern();奶栖,此時不論堆還是字符串常量池中都沒有"a"字符串對象,于是就會在字符串常量池中創(chuàng)建一個"a"字符串對象,接著再寫String s2=new String("a")宣鄙,堆中也會產(chǎn)生一個"a"字符串對象(注意袍镀,與第4條中情況不同,這種情況下JDK1.7的內(nèi)存中也會存在兩個"a"字符串對象)冻晤,但是苇羡,這之后不論調(diào)用s1.intern()還是s2.intern()返回的都是字符串常量池中的對象。反之亦可推導鼻弧,先寫String s1=new String("a")设江,并調(diào)用s1.intern(),以后不論以何種方式創(chuàng)建"a"字符串對象攘轩,并調(diào)用intern()方法叉存,字符串常量池中不會創(chuàng)建對象,并只會返回指向堆中“a”字符串對象的引用地址度帮。
? ? ? ? 理解這5點原理歼捏,上面這段代碼便迎刃而解了,s1.intern()后笨篷,JDK1.6會在字符串常量池中創(chuàng)建一個新的"a"字符串對象瞳秽,String s2=“a”時,發(fā)現(xiàn)字符串常量池中有"a"字符串率翅,直接使用該對象寂诱,所以s1指向堆中的對象,s2指向字符串常量池中的對象安聘,它們做“==”判斷當然為false;而在JDK1.7中s1.intern()不會在字符串常量池中創(chuàng)建新對象瓢棒,只會存入指向堆中"a"字符串對象的地址浴韭,那么String s2=“a”用的就是同一個地址,它們做“==”判斷當然為true脯宿。即使上述題目進行再復雜的變形念颈,掌握這5點基本底層原理,都能輕松分析出來连霉。
三榴芳、總結(jié)
? ? 本章節(jié)主要詳細介紹了Java虛擬機的內(nèi)存結(jié)構(gòu)以及各分區(qū)的功能,并著重剖析了方法區(qū)中的結(jié)構(gòu)跺撼,各種Java中的常量池各自的作用以及它們的區(qū)別窟感,下一章我們將一起學習Java虛擬機的內(nèi)存管理機制。??
《淺談Java虛擬機(二)—運行時數(shù)據(jù)區(qū)域》
《淺談Java虛擬機(四)—JVM調(diào)優(yōu)》
本系列文章參考文檔:《深入理解Java虛擬機:JVM高級特性與最佳實踐》--?周志明