雖然說了解虛擬機的運作并不是一般開發(fā)人員必須掌握的知識,但是對于中高級開發(fā)人員來說,如果不了解JVM一些技術(shù)特性的運行原理,就無法寫出更高效、更穩(wěn)定的代碼。并且在出現(xiàn)了內(nèi)存相關(guān)的問題時,如果不了解虛擬機是如何使用內(nèi)存的,那么進行錯誤排查及修復也會成為一個異常艱難的工作。本章將從JVM運行時區(qū)域和GC角度分析Java的內(nèi)存分配,希望對大家有所幫助。
運行時區(qū)域
Java虛擬機在執(zhí)行Java程序的過程中會把它管理的內(nèi)存區(qū)域劃分為若干個不同的數(shù)據(jù)區(qū)域。根據(jù)《java虛擬機規(guī)范》的規(guī)定,Java虛擬機所管理的內(nèi)存包括以下幾個運行時數(shù)據(jù)區(qū)域:

1.虛擬機棧
JVM棧是線程私有的內(nèi)存區(qū)域。它描述的是java方法執(zhí)行的內(nèi)存模型,每個方法執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每個方法從調(diào)用直至完成的過程,都對應著一個棧幀從入棧到出棧的過程。每當一個方法執(zhí)行完成時,該棧幀就會彈出棧幀的元素作為這個方法的返回值,并且清除這個棧幀,Java棧的棧頂?shù)臈褪钱斍罢趫?zhí)行的活動棧,也就是當前正在執(zhí)行的方法。就像是組成動畫的一幀一幀的圖片,方法的調(diào)用過程也是由棧幀切換來產(chǎn)生結(jié)果。
很多開發(fā)人員會把Java內(nèi)存分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack),這種劃分的流行只能說明大多數(shù)開發(fā)人員最關(guān)注、與對象內(nèi)存分配關(guān)系最密切的內(nèi)存區(qū)域是這兩塊,其中所指的“堆”在后面會講到,而所指的“?!本褪荍VM棧,或者說是JVM棧中的局部變量表部分。實際上Java內(nèi)存區(qū)域的劃分遠比這要復雜。
局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型(int、short、byte、char、double、float、long、boolean)、對象引用(reference類型,它不等同于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個代表對象的句柄或其他與此對象相關(guān)的位置)和returnAddress類型(指向了一跳字節(jié)碼指令的地址)。
在JVM規(guī)范中,對這個區(qū)域規(guī)定了兩種異常情況:如果線程請求的棧深度大于虛擬機允許的深度,將拋出StackOverflowError異常;如果虛擬機??梢詣討B(tài)擴展,在擴展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。
2.本地方法棧
本地方法棧和虛擬機棧所發(fā)揮的作用是很相似的,它們之間的區(qū)別不過是 虛擬機棧為虛擬機執(zhí)行Java方法(字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。Sun HotSpot 直接就把本地方法棧和虛擬機棧合二為一。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。
3.程序計數(shù)器
程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。在虛擬機概念模型里(概念模型,各種虛擬機可能會通過一些更高效的方式實現(xiàn)),字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令:分支、跳轉(zhuǎn)、循環(huán)、異常處理、線程恢復等基礎操作都會依賴這個計數(shù)器來完成。每個線程都有獨立的程序計數(shù)器,用來在線程切換后能恢復到正確的執(zhí)行位置,各條線程之間的計數(shù)器互不影響,獨立存儲。所以它是一個“線程私有”的內(nèi)存區(qū)域。此內(nèi)存區(qū)域是唯一一個在JVM規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
4.Java堆
對于大多數(shù)應用來說,Java堆(Heap)是JVM所管理的內(nèi)存中最大的一塊。它是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。主要用來存放對象實例,所有的對象實例以及數(shù)組都要在堆上分配。堆是垃圾收集器管理的主要區(qū)域,也被稱為“GC堆”,從內(nèi)存回收的角度來看,堆可以細分為:新生代和老年代;再細致一點可分為:Eden空間、From Survivor空間、To Survivor空間(空間分配比例是8:1:1)。如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,將拋出OutOfMemoryError異常。
5.方法區(qū)
方法區(qū)(Method Area)與堆一樣,也是各個線程共享的區(qū)域,存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),也就是用來存儲類的描述信息—元數(shù)據(jù)的。方法區(qū)是堆的一個邏輯部分,為了區(qū)分Java堆,它還有一個別名Non-Heap(非堆)。相對而言,GC對于這個區(qū)域的收集是很少出現(xiàn)的。當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常。
在Java 7及之前版本,我們也習慣稱它為“永久代”(Permanent Generation),更確切來說,應該是“HotSpot使用永久代實現(xiàn)了方法區(qū)”。需要注意的是,從Java 8開始,“永久代”已經(jīng)被徹底移除,使用了一個元空間(Metaspace)來代替它,后面我們會詳細講解。
6. 運行時常量池
運行時常量池是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池( Constant pool table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入運行時常量池中存放。運行時常量池相對于class文件常量池的另外一個特性是具備動態(tài)性,java語言并不要求常量一定只有編譯器才產(chǎn)生,也就是并非預置入class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中。
在近三個JDK版本(1.6、1.7、1.8)中, 運行時常量池(Runtime Constant Pool)的所處區(qū)域一直在不斷的變化,在JDK1.6時它是方法區(qū)的一部分;1.7又把他放到了堆內(nèi)存中;1.8之后出現(xiàn)了元空間,它又回到了方法區(qū)。其實,這也說明了官方對“永久代”的優(yōu)化從1.7就已經(jīng)開始了。
7.直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是JVM規(guī)范中定義的內(nèi)存區(qū)域。但這部分內(nèi)存也被頻繁的使用,而且也可能導致OutOfMemoryError異常出現(xiàn)。它在JDK中最直觀的表現(xiàn)就是NIO,基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復制數(shù)據(jù)。
從GC角度看Java堆
堆和方法區(qū)都是線程共享的區(qū)域,主要用來存放對象的相關(guān)信息。我們知道,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存也可能不一樣,我們只有在程序運行期間才能知道會創(chuàng)建哪些對象,因此, 這部分的內(nèi)存和回收都是動態(tài)的,垃圾收集器所關(guān)注的就是這部分內(nèi)存(本節(jié)后續(xù)所說的“內(nèi)存”分配與回收也僅指這部分內(nèi)存)。而在JDK1.7和1.8對這部分內(nèi)存的分配也有所不同,下面我們來詳細看一下
Java8中堆內(nèi)存分配如下圖:

從Java8開始,HotSpot已經(jīng)完全將永久代(Permanent Generation)移除,取而代之的是一個新的區(qū)域—元空間(MetaSpace),它使用本地內(nèi)存來存儲類元數(shù)據(jù)信息。也就是說,只要本地內(nèi)存足夠,它不會出現(xiàn)像永久代中“java.lang.OutOfMemoryError: PermGen space”這種錯誤。同樣的,對永久代的設置參數(shù) PermSize 和 MaxPermSize 也會失效。默認情況下,“元空間”的大小可以動態(tài)調(diào)整,或者使用新參數(shù)MaxMetaspaceSize 來限制本地內(nèi)存分配給類元數(shù)據(jù)的大小。
元空間特色
- 充分利用了Java語言規(guī)范:類及相關(guān)的元數(shù)據(jù)的生命周期與類加載器的一致。
- 每個類加載器都有它的內(nèi)存區(qū)域-元空間
- 只進行線性分配
- 不會單獨回收某個類(除了重定義類 RedefineClasses 或類加載失?。?/li>
- 沒有GC掃描或壓縮
- 元空間里的對象不會被轉(zhuǎn)移
- 如果GC發(fā)現(xiàn)某個類加載器不再存活,會對整個元空間進行集體回收
GC
- Full GC時,指向元數(shù)據(jù)指針都不用再掃描,減少了Full GC的時間。
- 很多復雜的元數(shù)據(jù)掃描的代碼(尤其是CMS里面的那些)都刪除了。
- 元空間只有少量的指針指向Java堆。這包括:類的元數(shù)據(jù)中指向java.lang.Class實例的指針;數(shù)組類的元數(shù)據(jù)中,指向java.lang.Class集合的指針。
- 沒有元數(shù)據(jù)壓縮的開銷
- 減少了GC Root的掃描(不再掃描虛擬機里面的已加載類的目錄和其它的內(nèi)部哈希表)
- G1回收器中,并發(fā)標記階段完成后就可以進行類的卸載
元空間內(nèi)存分配模型
- 絕大多數(shù)的類元數(shù)據(jù)的空間都從本地內(nèi)存中分配
- 用來描述類元數(shù)據(jù)的對象也被移除
- 為元數(shù)據(jù)分配了多個映射的虛擬內(nèi)存空間。
- 為每個類加載器分配一個內(nèi)存塊列表。
- 塊的大小取決于類加載器的類型
- Java反射的字節(jié)碼存取器(sun.reflect.DelegatingClassLoader )占用內(nèi)存更小
- 空閑塊內(nèi)存返還給塊內(nèi)存列表
- 當元空間為空,虛擬內(nèi)存空間會被回收
- 減少了內(nèi)存碎片
異常
在JVM規(guī)范的描述中,除了程序計數(shù)器以外,虛擬機內(nèi)存的其他幾個運行時區(qū)域都有發(fā)生OutOfMemoryError的可能。
| 運行時區(qū)域 | 異常 | 主要原因 |
|---|---|---|
| 虛擬機棧和本地方法棧 | StackOverflowError、OutOfMemoryError | StackOverflowError:線程請求的棧深度大于虛擬機所允許的最大深度;OutOfMemoryError:虛擬機在擴展棧時無法申請足夠的內(nèi)存空間 |
| 程序計數(shù)器 | 無 | 無 |
| 堆 | OutOfMemoryError | 對象數(shù)量到達最大堆的容量,內(nèi)存泄漏、內(nèi)存溢出 |
| 方法區(qū)和運行時常量池 | OutOfMemoryError | 反射,動態(tài)代理:CGLib、JSP、OSGI等 |
- 內(nèi)存泄露(Memory Leak):程序在申請內(nèi)存后,對象沒有被GC所回收,它始終占用內(nèi)存,內(nèi)存泄漏的堆積最終會造成內(nèi)存溢出。
- 內(nèi)存溢出(Memory Overflow):程序運行過程中無法申請到足夠的內(nèi)存而導致的一種錯誤。內(nèi)存溢出通常發(fā)生于OLD段或Perm段垃圾回收后,仍然無內(nèi)存空間容納新的Java對象的情況。通常都是由于內(nèi)存泄露導致堆棧內(nèi)存不斷增大,從而引發(fā)內(nèi)存溢出。