03.06 圖解Java內存區域

Java是一座圍城,Java開發不需要像C、C++開發人員那樣,維護每個對象從開始到終結的職責。因為Java虛擬機會幫助我們完成這些職責,但是一旦發生內存洩漏和溢出,就需要我們排查。

圖解Java內存區域

Java虛擬機執行Java程序時,把它管理的整個內存區域稱為運行時數據區。同時根據區域的用途,以及創建和銷燬時間等因素,將運行時數據區分成不同的區域。

圖解Java內存區域

程序計數器

程序計數器表示當前線程所執行字節碼指令的行號計數器。字節碼解釋器通過改變程序計數器的值,選取下一條需要執行的指令。為了保證線程切換之後恢復到正確的執行位置,每條線程都需要獨立的程序計數器,所以程序計數器是線程私有的。同時程序計數器是唯一一個在虛擬機規範中沒有規定 OutOfMemoryError 的區域。

注:線程執行Java方法,程序計數器記錄字節碼指令地址;如果執行的是本地(Native)方法,程序計數器為空。

圖解Java內存區域

虛擬機棧

虛擬機棧是Java方法執行的線程內存模型。每個方法的執行,Java虛擬機都會創建一個棧幀存儲方法相關變量。每個方法被調用到執行完畢的過程,對應棧幀在虛擬機棧中入棧到出棧的過程。

如下圖所示,當虛擬機執行 swap(a,b) 方法時,會創建一個單獨的棧幀 swap(a,b) 棧幀,在該棧幀中會存儲於方法相關的變量,該棧幀的入棧和出棧操作對應著方法的執行和結束。

圖解Java內存區域

每個棧幀都包含了局部變量表、操作數、動態鏈接、方法返回值。

  • 局部變量表:存放方法參數和內部定義的局部變量。局部變量表的容量以變量槽為最小單位每個變量槽可以存放一個 boolean 、 byte 、 char 、 short 、 int 、 float 、 reference 、 returnAddress 數據類型。
  • 操作數棧:底層也是棧結構,是進行數據運算的地方。當一個方法剛剛開始執行時,其操作數棧是空的,隨著方法執行和字節碼指令的執行,會從局部變量表或對象實例的字段中複製常量或變量寫入到操作數棧,再隨著計算的進行將棧中元素出棧到局部變量表或者返回給方法調用者,也就是出棧/入棧操作。
  • 動態鏈接:將常量池中指向方法的部分符號引用,在方法運行期間轉為直接引用。字節碼中的方法調用指令就是以常量池中指向方法的符號引用作為參數。這些符號引用一部分會在類加載階段或第一次使用時轉化為直接引用,這種轉化稱為靜態解析。另一部分將在每一次運行期間轉化為直接引用,這部分稱為動態連接。
  • 返回地址:方法執行退出後,返回到方法被調用的地方。
圖解Java內存區域

在 swap 函數執行的過程中, a 、 b 、 temp 都會保存到局部變量表中,其中的賦值操作則通過操作數棧執行,

方法執行完畢返回到調用的地方的地址則存儲在返回地址中。

圖解Java內存區域

本地方法棧

本地方法棧與 Java 虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的 Native 方法 服務。

Java堆

Java堆是虛擬機管理的內存中最大的一塊,幾乎所有對象都在Java堆分配內存。Java堆在虛擬機啟動的時候創建,被所有的線程共享。Java堆也會涉及到內存回收的內容,本片文章先不展開了。Java堆無法擴展時,會報出 OutOfMemoryError 異常。

圖解Java內存區域

方法區

方法區存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼緩存等數據。方法區是各個線程共享的內存區域。

圖解Java內存區域

屏幕面前的你,會不會遇到這樣的困惑。方法區和永久代有什麼關係?和元空間呢?

  • 方法區和永久代的關係方法區是JVM規範概念,而永久代則是HotSpot虛擬機特有的概念。《Java虛擬機規範》只是規定了有方法區的概念和作用,並沒有規定如何去實現它。那麼,在不同的 JVM 上方法區的實現肯定是不同的了。 同時大多數用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集擴展至方法區,或者說使用永久代來實現方法區。因此永久代是HotSpot的概念,方法區是Java虛擬機規範中的定義,是一種規範,而永久代是一種實現,一個是標準一個是實現。其他的虛擬機實現並沒有永久帶這一說法。在1.7之前在(JDK1.2 ~ JDK6)的實現中,HotSpot 使用永久代實現方法區,HotSpot 使用 GC分代來實現方法區內存回收,可以使用如下參數來調節方法區的大小。
  • 元空間對於Java8, HotSpots取消了永久代,取代永久代的就是元空間。永久代存在內存上限( -XX:MaxPermSize ,即使不設置也有默認大小),當進程申請不到足夠的內存,會造成內存溢出。改成元空間後,改用本地內存,只要本地空間足夠,就不會有內存溢出的問題。元空間和永久代有什麼不同的?存儲位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是連續的,而元空間屬於本地內存;存儲內容不同,元空間存儲類的元信息,靜態變量和常量池等併入堆中。相當於永久代的數據被分到了堆和元空間中。

運行時常量池

運行時常量池是方法區的一部分,是一塊內存區域。Class 文件常量池將在類加載後進入方法區的運行時常量池中存放。 一個類加載到 JVM 中後對應一個運行時常量池。

圖解Java內存區域

易混淆的概念

屏幕面前的你,會不會遇到這樣的困惑。運行時常量池和Class文件常量池有什麼關係?和字符串常量池呢?和緩衝池呢?

Class文件常量池

Class 文件常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。常量池中主要存放兩大類常量:字面量和符號引用。當Class文件常量池加載到方法區時,會把符號引用轉換為直接引用,存放到運行時常量池。

圖解Java內存區域

圖解Java內存區域

字符串常量池

字符串常量池是 全局的, JVM 中獨此一份 ,因此也稱為全局字符串常量池。

其中:

在 jdk1.6(含) 之前也是方法區的一部分,並且其中存放的是字符串的實例; 在 jdk1.7(含) 之後是在堆內存之中, 存儲的是字符串對象的引用,字符串實例是在堆中;

底層原理

在 HotSpot VM 裡實現線程池功能的是一個 StringTable 類,它是一個Hash表,默認值大小長度是1009;這個 StringTable 在每個 HotSpot VM 的實例只有一份,被所有的類共享。字符串常量由一個一個字符組成,放在了 StringTable 上。

<code>String str1 = "圖解Java";
String str2 = new String("圖解Java");
System.out.println(str1 == str2);/<code>

在這段代碼中,當執行 String str1 = "圖解Java" 時,先到常量池中查詢有沒有 "圖解Java" 字符串的引用,如果沒有,則會在 Java堆 上創建 "圖解Java" 字符串,在常量池中存儲字符串的地址, str1 則指向字符串常量池的地址。

String str2 = new String("圖解Java") ,則會直接在Java堆中創建對象。 str2 指向堆中的地址。

看到這裡,屏幕面前的你有沒有想到最後的結果是 false 呢。

如果此時還有 String str3 = "圖解Java" 那麼 str1==str3 的結果是什麼?

此時 str3 發現字符串常量池中已經有了 "圖解Java" 字符串的引用,則直接返回,不會創建新的對象。

看到這裡,屏幕面前的你有沒有想到最後的結果是 true 呢。

圖解Java內存區域

JVM 中除了字符串常量池,8種基本數據類型中除了兩種浮點類型剩餘的6種基本數據類型的包裝類,都使用了緩衝池技術,但是 Byte 、 Short 、 Integer 、 Long 、 Character 這5種整型的包裝類也只是在對應值在 [-128,127] 時才會使用緩衝池,超出此範圍仍然會去創建新的對象。

Class文件常量池、運行時常量池、字符串常量池的聯繫

我們平時寫好的Java代碼即Java格式的文件,經過編譯,會變成Class類型的文件。而Class文件有一部分是Class文件常量池,用於存儲字面量和符號引用。

Class文件經過類加載器加載後,之前Class文件常量池的內容會存放到方法區的運行時常量池,需要注意的是Class文件常量池的符號引用會轉變直接引用存入運行時常量池。

字符串常量池是 JVM 的一部分,整個 JVM 只有一份,在將Class文件常量池的字面量也會在類加載的時候進入到字符串常量池中。

份數內容Class文件常量池每個類對應一份字面量、符號引用運行時常量池每個類對應一份字面量、直接引用字符串常量池整個 JVM 僅有一份字符串

圖解Java內存區域


分享到:


相關文章: