Java內存區域與內存溢出異常詳解

修訂歷史

1.jvm內存模型圖:

在這裏插入圖片描述

2.程序計數器

  • 程序計數器(Program Counter
    Register)是一塊較小的內存空間,它可以看成是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏,字節碼解釋器工作就是通過改變這個計數器的值來選取下一條要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

  • 由於java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的指令位置,每條線程都需要有一個獨立的程序計數器,各條線程之間互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

cd /Volumes/code/java/test/springdemo/target/classes/com/example/springdemo/jvm/memory/heap
Vim HeapOutOfMemory.class      (vim顯示java二進制文件::%!xxd)
javap -verbose -p HeapOutOfMemory.class

3.java虛擬機棧

  • 與程序計數器一樣,Java虛擬機棧也是線程私有的,它的生命週期與線程相同。
    虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會常見一個棧
    幀用於存儲局部變量表、操作數棧、動態鏈接、方法出入口等信息。每一個方法從調用
    直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

  • 局部變量表存放了編譯器可知的各種基本類型(boolean、byte、char、short、int、
    float、long、double)、對象引用(reference類型,它不等同於對象本身,可能是一個
    指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關
    的位置)和returnAddress類型(指向了一條字節碼指令的地址)。

  • 在Java虛擬機規範中,對這個區域規定了兩種異常情況:如果線程請求的棧深度大於
    虛擬機允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前
    大部分的Java虛擬機都可以動態擴展,只不過Java虛擬機規範中也允許固定長度的虛擬機
    棧),如果擴展時無法申請到足夠的內容,就會拋出OutOfMemoryError異常。 (示例:StackOverFlow.java,
    HeapOutOfMemory.java)

在這裏插入圖片描述

4.本地方法棧

  • 本地方法棧(Native Method
    Stack)與虛擬機所發揮的作用是非常相似的,他們之間的區別不過是虛擬機棧爲虛擬機執行java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。在虛擬機規範中對本地方法棧中方法使用的語言、使用方式與數據結構沒有強制規定,因此具體的虛擬機可以自由實現它。

  • 與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowErrorOutOfMemoryError異常

5.java堆

  • 對於大多數應用來說,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。
    Java堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都是在這裏分配內存的。

  • Java堆事垃圾收集管理的主要區域,因此很多時候也被稱爲“GC堆”(Garbage Collected
    Heap)。從內存回收的角度來看,由於收集器基本都採用分代收集算法。所以Java堆中還可以 細分爲:新生代和老年代:再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。

  • 從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。不過無論如何劃分,都與存放內容無關,無論那個區域,存儲的都任然是對象實例,進一步劃分的目的是爲了更好地回收內存,或者更快地分配內存。

  • 根據Java虛擬機規定,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果在堆中沒有內存完成示例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。

6.方法區

  • 方法區(Method Area)與java堆一樣,時各個線程共享的內存區域,它用於存儲已被
    虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機
    規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的 是與Java堆區分開來。

  • Java虛擬機規範堆方法區的限制非常寬鬆,除了和Java堆一樣不需要連續的內存和可以
    選擇固定大小或可擴展外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名稱一樣“永久”存在了。這個區域的 內存回收目標主要是針對常量池的回收和對類型的卸載。

  • 根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

7.運行時常量

  • 運行時常量(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的
    版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),
    用於存放編譯器生成的各種字面量和符號引用1⃣️,這部分內容將在類加載後進入方法區的運 行時常量池中存放。

  • Java虛擬機對Class文件每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個
    字節用於存儲那種數據都必須符合規範上的要求才會被虛擬機認可、裝載和執行,但對於運行時常量池,Java虛擬機規範沒有做任何細節的要求,不同的提供商的虛擬機可以按照自己的需要來實現這個內存區域。不過,一半來說,除了保存Class文件描述的符號引用外,還會把翻譯 出來的直接引用也存儲在運行時常量池中。

  • 既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請 到內存時會拋出OutOfMemoryError異常。

8.直接內存

  • 直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是Java
    虛擬機規範中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致
    OutOfMemoryError異常出現,所以我們放到這裏一起講解。

  • 在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(channel)
    與緩衝區(buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一
    個存儲在Java堆中的DirectByteBuffer對象作爲這快內存的引用進行操作。這樣能在一些場
    景中顯著提高性能,因爲避免了在Java堆和Native堆中來回的複製數據。

  • 顯然,本季直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,肯定還
    是會受到本機總內存大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,
    會根據實際內存設置-Xmx等參數信息,但經常忽略直接內存,使得各個內存區域總和大於
    物理內存限制,從而導致動態擴展時出現OutOfMemoryError異常。

9.對象的創建

  • 對象的創建,通常就是new一個對象的過程,當虛擬機遇到一條new指令時,首先將去
    檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解釋和初始化。如果沒有,那必須先執行相應的類加載過程。 類加載之後就是虛擬機爲新生對象分配內存了。

內存分配有兩種分配方式:

  • 1.指針碰撞:假設java堆中的內存是絕對規整的,用一根指針作爲分界線來分割已分配 的內存和空閒的內存,所謂的分配就是把 指針指向空閒空間那邊挪動一段與對象大小一 樣的距離。

  • 2.空閒列表:假設java堆中的內存並不是規整的,已使用的和空閒的內存交錯在一起, 用一個列表來記錄那些內存是可用的,在分配的時候從列表中找到一塊足夠大的空間 劃分給對象實例,並更新列表上的記錄。
    選擇那種分配方案由java堆是否規整來決定,而java堆是否規整又由所採用的垃圾收集器 是否帶有壓縮整理的功能決定。

  • 3.在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header) 實例數據(Instance Data)和對其填充(Padding)。 HotSpot虛擬機的對象頭包括兩部分,第一部分用於存儲對象自身的運動時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有鎖、偏向鎖ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機中分別爲32bit和64bit,官方稱 它爲“Mark Word”。

  • 對象頭的另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過
    這個指針來確定這個對象是那個類的示例。並不是所有的虛擬機實現都必須在對象 數據上保留類型指針。

  • 接下來的實例數據部分是對象真正存儲的有效信息,也就是在程序代碼所定義的
    各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起
    來。HotSpot虛擬機默認的分配策略爲longs/doubles、ints、shorts/chars、bytes/booleans
    、oops(Ordinary Object Pointers),從分配策略來看,相同寬度的字段總是被分配到一起。
    在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。

  • 第三部分對其填充並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於HotSpotVM的自動內存管理系統要求對象起始地址必須是8字節的整數倍。

10.對象的訪問方式

  1. 建立對象是爲了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上
    的數據對象。由於reference類型在Java虛擬機規範中只規定了一個指向對象的引用,並沒
    有定義這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方式是取決於虛擬機實現而決定的。目前主流的訪問方式有使用句柄和直接指針兩種。

  2. 如果使用句柄訪問的話,那麼Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

  3. 如果使用直接指針訪問,那麼Java堆對象的佈局中就必須考慮如何放置訪問類型數 據的相關信息,而reference中存儲的直接就是對象地址。

  4. 這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存儲的是
    穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變 句柄中的實例數據指針,而reference本身不需要修改。

  5. 使用直接指針訪問方式的最大好處就是速度快,它節省了一次指針定位的時間開銷,
    由於對象的訪問在Java中非常頻繁,因此此類開銷積少成多後也是一項非常可觀的執行成 本。就虛擬機Sun HotSpot而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發 的範圍來看,各種語言和框架使用句柄來訪問的情況十分常見。

11.OutOfMemoryError異常

  • Java堆用於存儲對象實例,只要不斷創建對象,並保證GC Roots到對象之間有可
    達路徑來避免垃圾回收機制清除這些對象,那麼在數量達到最大堆的容量限制後 就會產生內存溢出異常。

12.虛擬機棧和本地方法棧溢出

  • HotSpot虛擬機中並不區分虛擬機棧和本地方法棧,棧容量由-Xss參數設定。
    如果虛擬機棧請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常
    如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常

13.本機內存溢出

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