新聞中心
怎樣理解JVM內(nèi)存模型,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。
成都創(chuàng)新互聯(lián)公司秉承實現(xiàn)全網(wǎng)價值營銷的理念,以專業(yè)定制企業(yè)官網(wǎng),成都網(wǎng)站制作、成都網(wǎng)站設計、外貿(mào)營銷網(wǎng)站建設,成都微信小程序,網(wǎng)頁設計制作,成都手機網(wǎng)站制作,網(wǎng)絡營銷推廣幫助傳統(tǒng)企業(yè)實現(xiàn)“互聯(lián)網(wǎng)+”轉(zhuǎn)型升級專業(yè)定制企業(yè)官網(wǎng),公司注重人才、技術和管理,匯聚了一批優(yōu)秀的互聯(lián)網(wǎng)技術人才,對客戶都以感恩的心態(tài)奉獻自己的專業(yè)和所長。
內(nèi)存區(qū)域
Java虛擬機在執(zhí)行Java程序的過程中會把他所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。Java虛擬機規(guī)范將JVM所管理的內(nèi)存分為以下幾個運行時數(shù)據(jù)區(qū):程序計數(shù)器,Java虛擬機棧,本地方法棧,Java堆,方法區(qū)。下面詳細闡述各數(shù)據(jù)區(qū)所存儲的數(shù)據(jù)類型。

程序計數(shù)器(Program Counter Register)
一塊較小的內(nèi)存空間,它是當前線程所執(zhí)行的子節(jié)碼的行號指示器,字節(jié)碼解釋器工作時通過改變該計數(shù)器的值來選擇下一條需要執(zhí)行的子節(jié)碼指令,分支、跳轉(zhuǎn)、循環(huán)等基礎功能都要依賴它來實現(xiàn)。每條線程都有一個獨立的程序計數(shù)器,各線程間的計數(shù)器互不影響,因此該區(qū)域是線程私有的。
當線程在執(zhí)行一個Java方法時,該計數(shù)器紀錄的是正在執(zhí)行的虛擬機字節(jié)嗎指令的地址,當線程在執(zhí)行的是Native方法(調(diào)用本地操作系統(tǒng)方法)時,該計數(shù)器的值為空。另外,該內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有任何OOM(內(nèi)存溢出:OutOfMemoryError)情況的區(qū)域。
Java虛擬機棧(Java Virtual Machine Stacks)
該區(qū)域也是線程私有的,它的生命周期也與線程相同。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會創(chuàng)建一個幀棧,棧它是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構。對于執(zhí)行引擎來講,活動線程中,只有棧頂?shù)臈怯行У模Q為當前棧,這個棧幀所關聯(lián)的方法稱為當前方法,執(zhí)行引擎所運行的所有字節(jié)碼都只針對當前的棧幀進行操作。棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時,棧幀中需要分配多少內(nèi)存,不會受到程序運行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機實現(xiàn)。在Java虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常情況:
如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常。 如果虛擬機在動態(tài)擴展棧時無法申請到足夠的內(nèi)存空間,則拋出OutOfMemory異常。
這兩種情況存在著一些互相重疊的部分:當棧空間無法繼續(xù)分配時,到底是內(nèi)存太小,還是已使用的棧空間太大,其本質(zhì)只是對同一件事情的兩種描述而已。其本質(zhì)上只是對一件事情的兩種描述而已。在單線程的操作中,無論是由于棧幀太大,還是虛擬機棧空間太小,當棧空間無法分配時,虛擬機拋出的都是StackOverflowError異常,而不會得到OutOfMemoryError異常。而在多線程環(huán)境下,則會拋出OutOfMemory異常。
下面詳細說明棧幀中所存放的各部分信息的作用和數(shù)據(jù)結(jié)構。
局部變量表是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量,其中存放的數(shù)據(jù)的類型是編譯期可知的各種基本數(shù)據(jù)類型、對象引用(reference)和returnAddress類型(它指向了一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,即在Java程序被編譯成Class文件時,就確定了所需分配的最大局部變量表的容量。當進入一個方法時,這個方法需要在棧中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。下面詳細說明棧幀中所存放的各部分信息的作用和數(shù)據(jù)結(jié)構。
1、局部變量表 局部變量表的容量以變量槽(Slot)為最小單位。在虛擬機規(guī)范中并沒有明確指明一個Slot應占用的內(nèi)存空間大小(允許其隨著處理器、操作系統(tǒng)或虛擬機的不同而發(fā)生變化),一個Slot可以存放一個32位以內(nèi)的數(shù)據(jù)類型:boolean、byte、char、short、int、float、reference和returnAddresss。reference是對象的引用類型,returnAddress是為字節(jié)指令服務的,它執(zhí)行了一條字節(jié)碼指令的地址。對于64位的數(shù)據(jù)類型(long和double),虛擬機會以高位在前的方式為其分配兩個連續(xù)的Slot空間。
虛擬機通過索引定位的方式使用局部變量表,索引值的范圍是從0開始到局部變量表最大的Slot數(shù)量,對于32位數(shù)據(jù)類型的變量,索引n代表第n個Slot,對于64位的,索引n代表第n和第n+1兩個Slot。
在方法執(zhí)行時,虛擬機是使用局部變量表來完成參數(shù)值到參數(shù)變量列表的傳遞過程的,如果是實例方法(非static),則局部變量表中的第0位索引的Slot默認是用于傳遞方法所屬對象實例的引用,在方法中可以通過關鍵字“this”來訪問這個隱含的參數(shù)。其余參數(shù)則按照參數(shù)表的順序來排列,占用從1開始的局部變量Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot。
局部變量表中的Slot是可重用的,方法體中定義的變量,作用域并不一定會覆蓋整個方法體,如果當前字節(jié)碼PC計數(shù)器的值已經(jīng)超過了某個變量的作用域,那么這個變量對應的Slot就可以交給其他變量使用。這樣的設計不僅僅是為了節(jié)省空間,在某些情況下Slot的復用會直接影響到系統(tǒng)的而垃圾收集行為。
2、操作數(shù)棧
操作數(shù)棧又常被稱為操作棧,操作數(shù)棧的最大深度也是在編譯的時候就確定了。32位數(shù)據(jù)類型所占的棧容量為1,64為數(shù)據(jù)類型所占的棧容量為2。當一個方法開始執(zhí)行時,它的操作棧是空的,在方法的執(zhí)行過程中,會有各種字節(jié)碼指令(比如:加操作、賦值元算等)向操作棧中寫入和提取內(nèi)容,也就是入棧和出棧操作。
Java虛擬機的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“棧”就是操作數(shù)棧。因此我們也稱Java虛擬機是基于棧的,這點不同于Android虛擬機,Android虛擬機是基于寄存器的。
基于棧的指令集最主要的優(yōu)點是可移植性強,主要的缺點是執(zhí)行速度相對會慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的優(yōu)點是執(zhí)行速度快,主要的缺點是可移植性差。
3、動態(tài)連接
每個棧幀都包含一個指向運行時常量池(在方法區(qū)中,后面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。Class文件的常量池中存在有大量的符號引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)。這些符號引用,一部分會在類加載階段或第一次使用的時候轉(zhuǎn)化為直接引用(如final、static域等),稱為靜態(tài)解析,另一部分將在每一次的運行期間轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接。
4、方法返回地址
當一個方法被執(zhí)行后,有兩種方式退出該方法:執(zhí)行引擎遇到了任意一個方法返回的字節(jié)碼指令或遇到了異常,并且該異常沒有在方法體內(nèi)得到處理。無論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行。方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時,調(diào)用者的PC計數(shù)器的值就可以作為返回地址,棧幀中很可能保存了這個計數(shù)器值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會保存這部分信息。
方法退出的過程實際上等同于把當前棧幀出站,因此退出時可能執(zhí)行的操作有:恢復上層方法的局部變量表和操作數(shù)棧,如果有返回值,則把它壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整PC計數(shù)器的值以指向方法調(diào)用指令后面的一條指令。
本地方法棧(Native Method Stacks)
該區(qū)域與虛擬機棧所發(fā)揮的作用非常相似,只是虛擬機棧為虛擬機執(zhí)行Java方法服務,而本地方法棧則為使用到的本地操作系統(tǒng)(Native)方法服務。
Java堆(Java Heap)
Java Heap是Java虛擬機所管理的內(nèi)存中的最大的一塊,它是所有線程共享的一塊內(nèi)存區(qū)域。幾乎所有的對象實例和數(shù)組都在這類分配內(nèi)存。Java Heap是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱為"GC堆"。
根據(jù)Java虛擬機的規(guī)定,Java堆可以處在物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。如果在堆中沒有內(nèi)存可分配時,并且堆也無法擴展時,將會拋出OutOfMemory。
方法區(qū)(Method Area)
方法區(qū)也是各個線程共享的內(nèi)存區(qū)域,它用于存儲已經(jīng)被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。方法區(qū)域又被稱為"永久代"。但著這僅僅對于Sun HotSpot來講,JRocket和IBMJ9虛擬機中并不存在永久代的概念。Java虛擬機規(guī)范把方法區(qū)描述為Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續(xù)的內(nèi)存,可以選擇固定大小或可擴展,另外,虛擬機規(guī)范允許該區(qū)域可以選擇不實現(xiàn)垃圾回收。相對而言,垃圾收集行為在這個區(qū)域比較少出現(xiàn)。該區(qū)域的內(nèi)存回收目標主要針是對廢棄常量的和無用類的回收。運行時常量池是方法區(qū)的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Class文件常量池),用于存放編譯器生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。運行時常量池相對于Class文件常量池的另一個重要特征是具備動態(tài)性,Java語言并不要求常量一定只能在編譯期產(chǎn)生,也就是并非預置入Class文件中的常量池的內(nèi)容才能進入方法區(qū)的運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用比較多的是String類的intern()方法。
根據(jù)Java虛擬機規(guī)范的規(guī)定,當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常。
直接內(nèi)存(Direct Memory)
直接內(nèi)存并不是虛擬機運行內(nèi)存時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域,它直接從操作系統(tǒng)中分配內(nèi)存,因此不受Java堆的大小的限制,但是會受到本機總內(nèi)存的大小及處理器尋址空間的限制,因此它也可能導致OutOfMemoryError異常出現(xiàn)。在Java1.4中新引入了NIO機制,它是一種基于通道與緩沖區(qū)的新I/O方式,可以直接從操作系統(tǒng)中分配直接內(nèi)存,可以直接從操作系統(tǒng)中分配直接內(nèi)存,即在堆外分配內(nèi)存,這樣能在一些場景中提高性能,因為避免了在Java堆和Native堆中來回復制數(shù)據(jù)。
內(nèi)存溢出
下面給出個內(nèi)存區(qū)域內(nèi)存溢出的簡單測試方法

這里有一點要重點說明,在多線程情況下,給每個線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存產(chǎn)生內(nèi)存溢出一場。操作系統(tǒng)為每個進程分配的內(nèi)存是有限制的,虛擬機提供了參數(shù)來控制Java堆和方法區(qū)這兩部分內(nèi)存的最大值,忽略掉程序計數(shù)器消耗的內(nèi)存(很小),以及進程本身消耗的內(nèi)存,剩下的內(nèi)存便給了虛擬機棧和本地方法棧,每個線程分配到的棧容量越大,可以建立的線程數(shù)量自然就越少。因此,如果是建立過多的線程導致的內(nèi)存溢出,在不能減少線程數(shù)的情況下,就只能通過減少最大堆和每個線程的棧容量來換取更多的線程。另外,由于Java堆內(nèi)也可能發(fā)生內(nèi)存泄露(Memory Leak),這里簡要說明一下內(nèi)存泄露和內(nèi)存溢出的區(qū)別:
內(nèi)存泄漏是指分配出去的內(nèi)存沒有被回收回來,由于失去了對該內(nèi)存區(qū)域的控制,因而造成了資源的浪費。Java中一般不會產(chǎn)生內(nèi)存泄漏,因為有垃圾回收器自動回收垃圾,但這也不絕對,當我們new了對象,并保存了其引用,但是后面一直沒用它,而垃圾回收器又不會去回收它,這就會造成內(nèi)存泄漏。
內(nèi)存溢出是指程序所需要的內(nèi)存超過了系統(tǒng)所能分配的內(nèi)存(包括動態(tài)擴展)的上限。
對象實例化分析
對內(nèi)存分配情況分析最常見的示例便是對象實例化:
Object obj = new Object();
這段代碼的執(zhí)行會涉及java棧、Java堆、方法區(qū)三個最重要的內(nèi)存區(qū)域。假設該語句出現(xiàn)在方法體中,及時對JVM虛擬機不了解的Java使用這,應該也知道obj會作為引用類型(reference)的數(shù)據(jù)保存在Java棧的本地變量表中,而會在Java堆中保存該引用的實例化對象,但可能并不知道,Java堆中還必須包含能查找到此對象類型數(shù)據(jù)的地址信息(如對象類型、父類、實現(xiàn)的接口、方法等),這些類型數(shù)據(jù)則保存在方法區(qū)中。
另外,由于reference類型在Java虛擬機規(guī)范里面只規(guī)定了一個指向?qū)ο蟮囊茫]有定義這個引用應該通過哪種方式去定位,以及訪問到Java堆中的對象的具體位置,因此不同虛擬機實現(xiàn)的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄池和直接使用指針。
通過句柄池訪問的方式如下:

通過直接指針訪問的方式如下:

這兩種對象的訪問方式各有優(yōu)勢,使用句柄訪問方式的最大好處就是reference中存放的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而reference本身不需要修改。使用直接指針訪問方式的最大好處是速度快,它節(jié)省了一次指針定位的時間開銷。目前Java默認使用的HotSpot虛擬機采用的便是是第二種方式進行對象訪問的。
看完上述內(nèi)容,你們掌握怎樣理解JVM內(nèi)存模型的方法了嗎?如果還想學到更多技能或想了解更多相關內(nèi)容,歡迎關注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!
本文題目:怎樣理解JVM內(nèi)存模型
文章分享:http://www.ef60e0e.cn/article/gjpepe.html