對于初學(xué)者來說谱邪,Java虛擬機就像一堆高墻领追,它用內(nèi)存動態(tài)分配和垃圾回收技術(shù)組成,隔離了Java和C的內(nèi)存管理的工作。今天就來一探究竟望蜡,一起翻越這堵高墻唤崭。
起因
相信很多童鞋都是只在學(xué)校里學(xué)過C、C++脖律,幾乎沒有接觸過Java谢肾。在使用C系列編程的時候,開發(fā)人員不得不自己去做對象內(nèi)存空間的申請和回收小泉,我們擁有無限大的權(quán)限芦疏,我們可以是“皇帝”,也同樣是每一個對象的“保姆”->負責(zé)每個對象的生命經(jīng)常的全部微姊。
Java的世界里酸茴,將這一切都屏蔽掉了,它底層使用一個叫虛擬機的東西自己實現(xiàn)了內(nèi)存的管理兢交。不用再寫alloc 和 free 這樣的函數(shù)了薪捍,不用日出現(xiàn)內(nèi)存泄露的問題。這一切看起來那么美好配喳,但是....正是因為這一切那么輕松酪穿,當(dāng)真正出現(xiàn)內(nèi)存泄露和溢出方面的問題的時候,如果我們不深入了解虛擬機是如何運作的晴裹、如果管理內(nèi)存的被济,那么一切都變成災(zāi)難!
示意圖
Java虛擬機在執(zhí)行Java程序的過程中會把它管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域涧团。這些區(qū)域都有各自的用途只磷,根據(jù)Java虛擬機規(guī)范的規(guī)定,虛擬機所管理內(nèi)存會被分為如下圖所示的幾個區(qū)域:
接下來就讓它們每個區(qū)域的區(qū)長上臺介紹下自己~~~
程序計數(shù)器
區(qū)長:其實我的地盤是比較小的少欺,其實大家可以把我看著是當(dāng)前線程所執(zhí)行的字節(jié)碼的交通信號燈喳瓣,負責(zé)控制每一條字節(jié)碼指令的執(zhí)行控制。我們這塊區(qū)域是授權(quán)唯一不會拋出OutOfMemoryError異常的區(qū)域
赞别!
字節(jié)碼解釋器工作時畏陕,其實就是通過改變信號燈的值來覺得下一個同行的是哪一條字節(jié)碼指令,分支仿滔、循環(huán)惠毁、跳轉(zhuǎn)、異常處理崎页、線程恢復(fù)等基礎(chǔ)功能都會依賴這個計數(shù)器完成鞠绰。
Java虛擬機的多線程內(nèi)部是通過線程輪流切換并分配CPU執(zhí)行時間的方式實現(xiàn)的,在任何一個確定的時刻飒焦,一個處理器都只會執(zhí)行一個線程鐘的指令蜈膨。那么當(dāng)線程切換后能恢復(fù)到上一次的位置屿笼,每個線程都需要一個獨立的程序計數(shù)器,各個線程之間互不干擾翁巍,這類線程共享的內(nèi)存區(qū)域稱為“線程私有”內(nèi)存驴一。
更多內(nèi)容:如果線程當(dāng)前執(zhí)行的是一個java方法,那么計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼命令的地址灶壶;如果是native的方法肝断,那計數(shù)器則為Undefined。
Java虛擬機棧
從示意圖我們可以看出來驰凛,這塊區(qū)域也是被劃為線程私有
內(nèi)存區(qū)域胸懈,當(dāng)線程結(jié)束的時候,這塊區(qū)域也就壽盡終寢啦恰响。虛擬機棧其實描述的正是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同事都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量趣钱、操作數(shù)、動態(tài)鏈接渔隶、方法出口這些信息羔挡。每一個方法從調(diào)用->執(zhí)行->返回的過程與棧幀在虛擬機棧中入棧到出棧的過程對應(yīng)。
幀棧結(jié)構(gòu)
局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型(boolean间唉、byte、char利术、short呈野、int、float印叁、long被冒、double)、對象引用(reference類型轮蜕,它不等同于對象本身昨悼,可能是指向一個代表對象的句柄或者其他與此對象相關(guān)的變量)和retureAddress類型。其中64位長度的long和double類型的數(shù)據(jù)會占2個局部變量空間跃洛,其余的數(shù)據(jù)類型只占1個率触。
局部變量表所需的內(nèi)存空間是在編譯期完成分配,當(dāng)進入一個方法時汇竭,這個方法需要在幀中分配多大的局部變量空間是完全確定的葱蝗,在方法運行期不會改變局部變量表的大小。
規(guī)范中定義了兩種異常:
- 如果線程的請求的棧深度大于虛擬機允許的深度细燎,將會拋出StackOverflowError異常
- 如果虛擬機棧動態(tài)可擴展两曼,當(dāng)擴展到無法申請到足夠內(nèi)存時,就會拋出OutOfMemoryError異常
本地方法棧
這塊內(nèi)存區(qū)域和虛擬機棧非常相似玻驻,他們的區(qū)別從名字就可以看出來:Java虛擬機棧是用來執(zhí)行Java方法的悼凑,而本地方法棧是用來執(zhí)行Native方法的。
Sun HotSpot 合并了虛擬機棧和本地方法棧
Java堆
Java堆應(yīng)該是虛擬機管轄范圍內(nèi)最大的一塊內(nèi)存區(qū)域。這塊區(qū)域是被所有線程共享户辫,在虛擬機一啟動的時候就創(chuàng)建的渐夸,它的唯一目的就是存放對象實例,幾乎所有對象的實例都需要到這里申請內(nèi)存寸莫。
規(guī)范中描述:所有的對象實例以及數(shù)組都要在堆上分配捺萌,但是隨著JIT編譯器和逃逸分析技術(shù)的成熟,棧上分配膘茎、標(biāo)量替換這些技術(shù)會導(dǎo)致一些微妙的變化桃纯,前面的“所有”會變得不那么絕對。
Java堆是垃圾回收期(GC)管理的主要區(qū)域披坏,從內(nèi)存回收的角度來看态坦,現(xiàn)代的收集器都采用分代收集算法,所有這塊區(qū)域又會細分為好幾個區(qū)域:新生代棒拂、老年代伞梯;再細節(jié)的又會分為 Eden、From Survivor帚屉、To Survivor谜诫,下圖展示了詳細的區(qū)域:
從內(nèi)存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(qū)攻旦。但是不管怎么劃分喻旷,都與它存放的內(nèi)容(對象實例)無關(guān),都是為了更好地管理這塊區(qū)域牢屋,進行內(nèi)存回收且预、更快分配內(nèi)存
。
規(guī)范中規(guī)定烙无,Java堆的內(nèi)存可以不保證空間連續(xù)锋谐,只需要保證邏輯連續(xù)即可。所以我們在實際生產(chǎn)中經(jīng)常講-Xmx和-Xms寫成一致的截酷,這樣來減低因為動態(tài)擴展帶來的不可預(yù)見情況
方法區(qū)
這塊區(qū)域在java8中已經(jīng)被換成元空間涮拗,詳情見JAVA8:從永久區(qū)PermGen到元空間Metaspace
方法區(qū)和Java堆一樣,都是所有線程共享的一塊內(nèi)存區(qū)域合搅,它用于存儲已被虛擬機加載的類信息多搀、常量、靜態(tài)變量灾部、即時編譯器編譯后的代碼等數(shù)據(jù)康铭。另外它有一個別名叫著“永久代”,這是因為HotSpot虛擬機將GC分代收集擴展到了方法區(qū)赌髓,這樣HotSpot的垃圾回收器就可以一同樣的方式來管理這塊內(nèi)存从藤。而這塊區(qū)域會因為極少數(shù)情況(深入理解 Java String#intern() 內(nèi)存模型)出現(xiàn)異常
運行時常量池
這部分也是方法區(qū)的一部分催跪,Class文件中除了有類的版本號、字段夷野、方法懊蒸、接口等描述信息外,還有一項信息是常量池悯搔,用于存放編譯期生成的各種字面量和符號引用骑丸,這部分內(nèi)容將在類加載后進入方法去的運行時常量池中存放。
直接內(nèi)存
這塊區(qū)域在上面的示意圖中看不到妒貌,是因為這塊內(nèi)存其實不受虛擬機管理通危,當(dāng)時這部分內(nèi)存過度使用,也會拋出OutOfMemoryError異常灌曙。
NIO中引入了一種基于通信和緩沖區(qū)的I/O方式菊碟,它可以使用Native的函數(shù)來直接分配Java堆外的內(nèi)存,然后通過一個存儲在Java堆中的一個DirectByteBuffer對象作為這塊內(nèi)存引用進行操作在刺。所以在設(shè)置虛擬機堆大小的時候需要注意逆害,如果使用了堆外內(nèi)存需要考慮公式:堆內(nèi)+堆外 < 總內(nèi)存大小。目前市面上有很多的堆外工具蚣驼,mapdb就是其中之一魄幕。應(yīng)用比較多的是堆外緩存解決使用堆內(nèi)緩存造成的fullgc頻繁問題。
參考
--