Java內存區域和內存溢出異常(深入理解Java虛擬機)

運行時數據區域

JVM在執行程序時會把管理的內存劃分爲若干個不同的數據區域。

運行時數據區包括以下:

  1. 方法區(Method Area)——所有線程共享數據
  2. 堆(Heap)——所有線程共享數據
  3. 程序計數器(Program Counter Register)——線程私有
  4. 虛擬機棧(VM Stack)——線程私有
  5. 本地方法棧(Native Method Stack)——線程私有

以上的運行時數據區,對外連接執行引擎、本地庫接口、本地方法庫

運行時數據區域

程序計數器

Program Counter Register,一塊較小的內存空間,可以看作是當前線程所執行代碼的字節碼的行號指示器。在JVM模型中,字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼,分支、循環、跳轉、try-catch、線程恢復等基礎功能都需要這個計數器來完成。
Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間來實現的。爲了線程切換後能恢復到正確的位置,每條線程都需要一個獨立的程序計數器。線程之間互不影響、獨立存儲(這類內存區域就叫線程私有內存)
線程執行一個Java方法時,計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果執行的是Native方法,計數器爲undefined。此內存區域是爲唯一一個在JVM規範中沒有規定任何OutOfMemoryError情況的區域。

Java虛擬機棧(JVM Stack)

私有的,生命週期與線程相同。JVM Stack描述的是Java方法執行的內存模型:每個方法執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表大小在編譯期就被確定,包括編譯期可知的各種基本數據類型(boolean–double)、對象引用(引向對象起始地址的引用指針,或者指向一個代表對象的句柄活其他與此對象相關的位置)和返回地址(retureAddress,指向了一條字節碼指令的地址),其中long和double佔用兩個slot(局部變量空間,應該是4byte),其他佔用一個(注意,reference也是一個int的長度)。當執行方法的時候,需要在Stack Frame分配多大的局部變量空間是完全確定的,運行期間不會改變局部變量表的大小。
JVM Stack有兩種異常,當線程請求的棧深度大於虛擬機所允許的深度,跑出StackOverflowError,如果可以動態擴展但無法申請足夠的內存,就會拋出OutOfMemoryError。

本地方法棧

與Java虛擬機棧類似,不過JVM Stack是爲執行字節碼服務器,Native Method Stack則爲虛擬機使用Native方法服務。虛擬機規範中對本地方法棧的語言、使用方式和數據結構沒有強規定。Native Method Stack也會拋出StackOverflowError和OutOfMMemoryError。

Java Heap

Java堆是最大的一塊,並且是所有線程共享的,在虛擬機啓動時創建。存放所有的對象實例。在Java虛擬機規範中描述是:所有的對象實例以及數組都要在堆上分配。但由於JIT編譯器逃逸分析技術,也不是所有的實例都在堆上。
Java堆是垃圾收集器管理的主要區域,所以有時候也叫GC堆(Garbage Collected Heap),現在GC一般採用分代收集算法,所以Heap都細分爲新生代和老年代。線程共享的Java堆可能劃分出多個線程私有的分配緩衝區。
JVM規範中,Java Heap可以處於物理上不連續,但邏輯上連續的內存空間。當前主流VM都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果在堆中沒有內存完成實例分配,並且堆無法擴展時,拋出OutOfMemoryError。

方法區

Method Area與Java堆一樣,是各個線程共享的內存區域。用來存放被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。JVM規範把方法區描述爲堆的一個邏輯部分,但有一個別名叫Non-Heap,目的應該爲了與Java Heap區分開。
HotSpot虛擬機把GC分代收集擴展到方法區,所以把方法區叫做永久代。
垃圾收集行爲在這個區域是比較少見,但並非數據進了方法區就真的永久存在。這個區域內存回收主要是針對常量池的回收和類型的卸載,但回收效果一般,特別是類型卸載條件相當苛刻。
JVM規範規定當方法區無法滿足內存分配需求時,拋出OutOfMemoryError。

運行時常量池

Runtime Constant Pool,是方法區的一部分,
Class文件中除了有類的版本、字段、方法和接口等描述信息外,還有一項是常量池。用於存放編譯期產生的各種字面量和符號引用,這部分內容將在類加載後進入方法區運行時常量池中存放。比如String s = “java”;在編譯時會優化,多個相同的常量會引用同一個。關於常量池

運行時常量池相對於Class文件常量池,一個重要特性就是具備動態性。Java語言並不要求常量一定是編譯期才能產生,也就是並非預置入class文件中常量池的內容才能進入方法區運行時常量池。運行期間產生的新的常量也能放入池中,比如String類的intern()方法

直接內存

直接內存(Direct Memory)並不是JVM運行時數據區的一部分,也不是JVM規範中定義的內存區域。但這部分內存被頻繁使用,而且也可能導致OutOfMemoryError。
JDK1.4中加入了NIO(New Input/Output)類,引入了基於通道(Channel)和緩衝區(Buffer)的I/O方式,可以使用Native函數庫直接分配堆外的內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。能避免Java堆和Native堆來回複製數據。
直接內存的分配不受Java堆大小的限制,但受到本機總內存(RAM和SWAP或者分頁文件)大小和處理器尋址空間的限制,當各內存區域總和大於物理內存限制,會導致動態擴展時出現OutOfMemoryError。

一篇不錯的筆記

一篇不錯的筆記

後記

native heap,不屬於JVM運行時數據區,或者說可以理解爲JVM之外的OS Memory的區域,或者說JNI方法分配的內存??。
stackoverflow上的討論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章