1. 程式人生 > >Java內存管理-JVM內存模型以及JDK7和JDK8內存模型對比總結(三)

Java內存管理-JVM內存模型以及JDK7和JDK8內存模型對比總結(三)

區域 程序 ive 大小 如果 連續 物理 空間 http

勿在流沙住高臺,出來混遲早要還的。

做一個積極的人

編碼、改bug、提升自己

我有一個樂園,面向編程,春暖花開!

上一篇分享了JVM及其啟動流程,今天介紹一下JVM內部的一些區域,以及具體的區域在運行過程中會發生哪些異內存常! 其實也就對應了內存管理的第一篇中 JVM的第三個階段,程序運行內存溢出。

知識地圖

技術分享圖片

一、概述

Java的內存管理采用[自動內存管理]機制,因為這個自動管理機制,Java程序員就不需要去寫釋放內存的代碼,而且不容易出現內存泄漏問題(比C/C++程序員少一些煩惱)。但是由於內存的申請和釋放都交給了Java虛擬機,一旦出現內存泄漏和溢出問題時,在不了解Java虛擬機內存結構和自動管理機制的情況下,就很難排查問題的所在。所以如果想要成為一個優秀的程序員或者進階為一個牛逼的架構師,掌握Java虛擬機的自動內存管理機制那是必須的。

二、JVM運行時數據區域

根據《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括以下幾個運行時的數據區域:程序計數器(Program Counter Register)、Java棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(Method Area)、堆(Heap)。

如圖所示:

技術分享圖片

註:上圖的虛擬機運行時數據區是Java虛擬機規範所規定的區域,不同的虛擬機有不同的實現。

上面圖片有線程共享和線程隔離的區域,下面在通過一張圖片來進行簡單說明,讓你更加清晰的理解什麽是線程共享和什麽是線程隔離。

技術分享圖片

通過上面的兩個圖,大概對JVM的內存模型有個初步的認識,下面我們在看一下具體的每一個區域到底是什麽東東。

1、程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它可以是看作當前線程所執行的字節碼的行號指示器。說簡單一點就是一個計數器,當字節碼解釋器工作是能夠通過改變這個計數器的值來選取下一條需要執行的字節碼指令。在說明一點,各條線程之間計數器互不影響,獨立存儲,程序計數器器內存區域為 線程私有 的。

類比匯編語言中的程序計數器:在匯編語言中,程序計數器是指CPU中的寄存器,它保存的是程序當前執行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址),當CPU需要執行指令時,需要從程序計數器中得到當前需要執行的指令所在存儲單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程序計數器便自動加1或者根據轉移指針得到下一條指令的地址,如此循環,直至執行完所有的指令。

在JVM規範中規定,如果線程執行的是非native方法,則程序計數器中保存的是當前需要執行的指令的地址;如果線程執行的是native方法,則程序計數器中的值是undefined。由於程序計數器中存儲的數據所占空間的大小不會隨程序的執行而發生改變,因此,此內存區域是唯一一個在JVM規範中沒有規定任何OutOfMemoryError情況的區域

2.本地方法棧

本地方法棧和虛擬機棧所發揮的作用是很相似的,它們之間的區別不過是 虛擬機棧為虛擬機執行Java方法(字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。Sun HotSpot 直接就把本地方法棧和虛擬機棧合二為一。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。

3、Java虛擬機棧

 Java棧也稱作虛擬機棧(Java Vitual Machine Stack),也是常說的棧。Java棧是Java方法執行的內存模型。Java棧中存放的是一個個的棧幀,每個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操作數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。棧也是線程私有的。

下圖表示了一個Java棧的模型

技術分享圖片

技術分享圖片

1)、局部變量表

就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的。

2)、操作數棧

想必學過數據結構中的棧的朋友想必對表達式求值問題不會陌生,棧最典型的一個應用就是用來對表達式求值。想想一個線程執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麽說,程序中的所有計算過程都是在借助於操作數棧來完成的。

3)、指向運行時常量池的引用

因為在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。

4)、方法返回地址

當一個方法執行完畢之後,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址。由於每個線程正在執行的方法可能不同,因此每個線程都會有一個自己的Java棧,互不幹擾。也就解釋了棧是線程私有的。

當線程執行一個方法時,就會隨之創建一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。因此可知,線程當前執行的方法所對應的棧幀必定位於Java棧的頂部。在這個區域規定了兩種異常狀況:

  • 如果線程請求的棧深入大於虛擬機所允許的深度,將拋出StackOverFlowError異常!
  • 如果虛擬機棧可以動態擴展,當擴展到無法申請內存到足夠的內存,就會拋出OutOfMemoryError異常!

4、Java堆

堆是jvm內存管理的最大的一塊區域,此內存區域的唯一目的就是存放對象的實例,所有對象實例與數組都要在堆上分配內存。它也是垃圾收集器的主要管理區域。java對可以處於物理上不連續的空間,只要邏輯上是連續的即可。線程共享的區域。如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將拋出OutOfMemoryError異常。

為了支持垃圾收集,堆被分為三個部分:

  • 年輕代 : 常常又被劃分為Eden區和Survivor(From Survivor To Survivor)區(Eden空間、From Survivor空間、To Survivor空間(空間分配比例是8:1:1

  • 老年代

  • 永久代 (jdk 8已移除永久代,下面會講解)

技術分享圖片

1) 堆是JVM中所有線程共享的,因此在其上進行對象內存的分配均需要進行加鎖,這也導致了new對象的開銷是比較大的 (2) Sun Hotspot JVM為了提升對象內存分配的效率,對於所創建的線程都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行的情況計算而得,在TLAB上分配對象時不需要加鎖,因此JVM在給線程的對象分配內存時會盡量的在TLAB上分配,在這種情況下JVM中分配對象內存的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用堆空間分配 (3) TLAB僅作用於新生代的Eden Space,因此在編寫Java程序時,通常多個小的對象比大的對象分配起來更加高效。

(4) 所有新創建的Object 都將會存儲在新生代Yong Generation中。如果Young Generation的數據在一次或多次GC後存活下來,那麽將被轉移到OldGeneration。新的Object總是創建在Eden Space。

這些知識在後面學習GC和內存調優方面非常重要。

5、方法區

方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。方法區是堆的一個邏輯部分,為了區分Java堆,它還有一個別名Non-Heap(非堆)。相對而言,GC對於這個區域的收集是很少出現的。當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

在Java 7及之前版本,我們也習慣稱方法區它為“永久代”(Permanent Generation),更確切來說,應該是“HotSpot使用永久代實現了方法區”!

6、運行時常量池

運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池( Constant pool table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入運行時常量池中存放。運行時常量池相對於class文件常量池的另外一個特性是具備動態性,java語言並不要求常量一定只有編譯器才產生,也就是並非預置入class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中。

7、直接內存

直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是JVM規範中定義的內存區域。但這部分內存也被頻繁的使用,而且也可能導致OutOfMemoryError異常出現

JDK1.4中新引入了NIO機制,它是一種基於通道與緩沖區的新I/O方式,可以直接從操作系統中分配直接內存,即直接堆外分配內存,這樣能在一些場景中提高性能,因為避免了在Java堆和Native堆中來回復制數據。

三、JDK7和JDK8的JVM內存模型的總結

1、方法區變化

這裏介紹的是JDK1.8 JVM內存模型。1.8同1.7比,最大的差別就是:元數據區取代了永久代,就是JDK8沒有了PermSize相關的參數配置了。元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元數據空間並不在虛擬機中,而是使用本地內存

1)方法區與永久代的區別?

方法區只是JVM規範定義,而永久代為具體的實現,元空間也是方法區在jdk1.8中的一種實現。

2)為什麽廢除永久代?

1.官方文檔:移除永久代是為融合HotSpot JVM與 JRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代

2..PermGen很難調整,PermGen中類的元數據信息在每次FullGC的時候可能被收集,但成績很難令人滿意。

而且應該為PermGen分配多大的空間很難確定,因為PermSize的大小依賴於很多因素,比如JVM加載的class總數,常量池的大小,方法的大小等。

並且永久代內存經常不夠用發生內存泄露。

2、運行時常量池變化

在近三個JDK版本(1.6、1.7、1.8)中, 運行時常量池(Runtime Constant Pool)的所處區域一直在不斷的變化,在JDK1.6時它是方法區的一部分;1.7又把他放到了堆內存中;1.8之後出現了元空間,它又回到了方法區。其實,這也說明了官方對“永久代”的優化從1.7就已經開始了。

技術分享圖片

貼一張 Java 8 的內存模型圖:

技術分享圖片

四、總結

運行時區域大概了解後,我們在來總結一下:

| 運行時區域 | 異常 | 主要原因 | | -------------------- | ------------------------------------ | ------------------------------------------------------------ | | 虛擬機棧和本地方法棧 | StackOverflowError、OutOfMemoryError | StackOverflowError:線程請求的棧深度大於虛擬機所允許的最大深度;OutOfMemoryError:虛擬機在擴展棧時無法申請足夠的內存空間 | | 程序計數器 | 無 | 無 | | 堆 | OutOfMemoryError | 對象數量到達最大堆的容量,內存泄漏、內存溢出 | | 方法區和運行時常量池 | OutOfMemoryError | 反射,動態代理:CGLib、JSP、OSGI等 |

最後在說兩個概念:

內存泄露(Memory Leak):程序在申請內存後,對象沒有被GC所回收,它始終占用內存,內存泄漏的堆積最終會造成內存溢出。

內存溢出(Memory Overflow):程序運行過程中無法申請到足夠的內存而導致的一種錯誤。內存溢出通常發生於OLD段或Perm段垃圾回收後,仍然無內存空間容納新的Java對象的情況。通常都是由於內存泄露導致堆棧內存不斷增大,從而引發內存溢出。

本文內容較多,也比較重要,因為本人也是在學習JVM中,如果文中有不對的地方,也請指出!謝謝!

當我們了解了JVM和JVM裏面有什麽區域,對後面學習和理解JVM的一些規範或者排查JVM相關的問題也更加容易一點,當我們知道了JVM哪些地方報什麽樣的錯誤的時候,在出現問題的時候,能夠快速的定位和解決,這樣對於我們的成長來說幫助也是非常大的,繼續學習JVM,深入對JVM的認識,也了解更加有趣的Java世界。

五、參考內容

《深入理解Java虛擬機》

JDK1.8 JVM內存模型

Java內存區域與內存溢出異常(jdk 6,7,8)

對於JVM內存模型的理解(對比jdk1.7與1.8)


謝謝你的閱讀,如果您覺得這篇博文對你有幫助,請點贊或者喜歡,讓更多的人看到!祝你每天開心愉快!


不管做什麽,只要堅持下去就會看到不一樣!在路上,不卑不亢!

博客首頁 : http://blog.csdn.net/u010648555

願你我在人生的路上能都變成最好的自己,能夠成為一個獨擋一面的人技術分享圖片

© 每天都在變得更好的阿飛雲

Java內存管理-JVM內存模型以及JDK7和JDK8內存模型對比總結(三)