Java內存區域(運行時數據區域)詳解、JDK1.8與JDK1.7的區別


2.1 概述

對Java程序員來說,在虛擬機自動內存管理機制的幫助下,不再需要為每個對象的new操作去寫配對的delete/free 代碼,不容易出現內存泄露和內存溢出的問題。不過,仍然需要Java虛擬機是如何使用內存的,方便我們定位內存泄露和內存溢出的問題:

2.2 運行時數據區域

Java虛擬機在執行Java程序時會把它所管理的內存划分為若干個部分,這些區域有各自的用途、創建和銷毀時間,有的區域隨着虛擬機進程的啟動而一直存在,有的則依賴用戶線程的啟動和結束而建立和銷毀。

JDK 1.8 之前的內存分布(圖為粘貼所得):

 

JDK 1.8 之后的內存分布:

2.2.1 程序計數器

當前線程所執行的字節碼的行號指示器,是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴程序計數器。內存較小。

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

如果線程執行的是Java方法,則記錄的是正在執行的虛擬機字節碼指令的地址。如果是Native 本地方法,計數器值為空(Undefined)

從上面的介紹中我們知道程序計數器主要有兩個作用:

  1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
  2. 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

此內存區域是唯一一個在《java虛擬機規范》中沒有規定任何 OOM 情況的區域

2.2.2 Java 虛擬機棧

線程私有,Java虛擬機棧的生命周期與線程相同。

Java虛擬機棧描述的是Java方法執行的線程內存模型:每個方法被執行的時候,Java虛擬機都會同步創建一個棧幀用於保存 局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法被調用直至執行完畢的過程,對應了棧幀在虛擬機棧中入棧到出棧的過程。

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

Java 虛擬機棧會出現兩種錯誤:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態擴展,那么當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 錯誤。
  • OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 錯誤。

Java 方法有兩種返回方式:return 語句;拋出異常,不管哪種返回方式都會導致棧幀被彈出。

2.2.3 本地方法棧

和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。

2.2.4 Java堆

Java 虛擬機所管理的內存中最大的一塊,Java 堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。

Java世界中“幾乎”所有的對象都在堆中分配,但是,隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么“絕對”了。從jdk 1.7開始已經默認開啟逃逸分析,如果某些方法中的對象引用沒有被返回或者未被外面使用(也就是未逃逸出去),那么對象可以直接在棧上分配內存。

Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap)。從垃圾回收的角度,由於現在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代:再細致一點將新生代分為:Eden 空間、From Survivor、To Survivor 空間。進一步划分的目的是更好地回收內存,或者更快地分配內存。

在 JDK 7 版本及JDK 7 版本之前,堆內存被通常被分為下面三部分:

  1. 新生代內存(Young Generation)
  2. 老生代(Old Generation)
  3. 永生代(Permanent Generation)

JVM堆內存結構-JDK7

JDK 8 版本之后方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。

JVM堆內存結構-JDK8

 從內存分配的角度看,所有線程共享的java堆可以划分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB),以提升分配的效率。根據《Java虛擬機規范》,Java堆可以處於物理上並不連續的內存空間,但在邏輯上可視為連續的。

堆這里最容易出現的就是 OutOfMemoryError 錯誤,比如:

  1. OutOfMemoryError: GC Overhead Limit Exceeded : 當JVM花太多時間執行垃圾回收並且只能回收很少的堆空間時,就會發生此錯誤。
  2. java.lang.OutOfMemoryError: Java heap space :假如在創建新的對象時, 堆內存中的空間不足以存放新創建的對象, 就會引發java.lang.OutOfMemoryError: Java heap space 錯誤。(和本機物理內存無關,和你配置的內存大小有關!)

2.2.5 方法區

線程共享,用於保存已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等數據。雖然 Java 虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。

方法區和永久代的關系

《Java 虛擬機規范》只是規定了有方法區這個概念和它的作用,並沒有規定如何去實現它。那么,在不同的 JVM 上方法區的實現肯定是不同的了。 方法區和永久代的關系很像 Java 中接口和類的關系,類實現了接口,而永久代就是 HotSpot 虛擬機對虛擬機規范中方法區的一種實現方式,當時的HotSpot 虛擬機設計團隊選擇把收集器的分代設計擴展到方法區。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機規范中的定義,是一種規范,而永久代是一種實現,一個是標准一個是實現,其他的虛擬機實現並沒有永久代這一說法。

為什么要將永久代 (PermGen) 替換為元空間 (MetaSpace) 呢?

  1. 整個永久代有一個 JVM 本身設置固定大小上限,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,雖然元空間仍舊可能溢出,但是比原來出現的幾率會更小。

當元空間溢出時會得到如下錯誤: java.lang.OutOfMemoryError: MetaSpace

你可以使用 -XX:MaxMetaspaceSize 標志設置最大元空間大小,默認值為 unlimited,這意味着它只受系統內存的限制。-XX:MetaspaceSize 調整標志定義元空間的初始大小如果未指定此標志,則 Metaspace 將根據運行時的應用程序需求動態地重新調整大小。

  1. 元空間里面存放的是類的元數據,這樣加載多少類的元數據就不由 MaxPermSize 控制了, 而由系統的實際可用空間來控制,這樣能加載的類就更多了。

  2. 在 JDK8,合並 HotSpot 和 JRockit 的代碼時, JRockit 從來沒有一個叫永久代的概念, 合並之后就沒有必要額外的設置這么一個永久代的地方了。

方法區的發展遷移過程

JDK 6 時,HotSpot 團隊就有放棄永久代、逐步改為本地內存來實現方法區的計划了。JDK 7 ,已經把原本放在永久代的字符串常量池、靜態變量等移除。JDK8,終於完全放棄了永久代,把JDK 7 中永久代還剩余的內容(主要是類型信息)全部移到元空間中。

根據《Java虛擬機規范》,如果方法區無法滿足新的內存分配需求時,將拋出 OOM 異常。

2.2.6 字符串常量池

運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池表(用於存放編譯期生成的各種字面量和符號引用)

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

JDK1.7 及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池。

修正(issue747reference):

  1. JDK1.7之前運行時常量池邏輯包含字符串常量池存放在方法區, 此時hotspot虛擬機對方法區的實現為永久代
  2. JDK1.7 字符串常量池被從方法區拿到了堆中, 這里沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區, 也就是hotspot中的永久代 
  3. JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆, 運行時常量池還在方法區, 只不過方法區的實現從永久代變成了元空間(Metaspace)

2.2.7 直接內存

直接內存並不是虛擬機運行時數據區的一部分,也不是虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現。

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

本機直接內存的分配不會受到 Java 堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM