面試問題:你瞭解Java內存模型麼(Java7、8、9內存模型的區別)

 

Java內存模型是每個java程序員必須掌握理解的,這是Java的核心基礎,對我們編寫代碼特別是併發編程時有很大幫助。由於Java程序是交由JVM執行的,所以我們在談Java內存區域劃分的時候事實上是指JVM內存區域劃分。

首先,我們回顧一下Java程序執行流程:

如上圖所示,首先Java源代碼文件(.java後綴)會被Java編譯器編譯爲字節碼文件(.class後綴),然後由JVM中的類加載器加載各個類的字節碼文件,加載完畢之後,交由JVM執行引擎執行。在整個程序執行過程中,JVM會用一段空間來存儲程序執行期間需要用到的數據和相關信息,這段空間一般被稱作爲Runtime Data Area(運行時數據區),也就是我們常說的JVM內存。因此,在Java中我們常常說到的內存管理就是針對這段空間進行管理(如何分配和回收內存空間)。

 那麼本篇文章主要是要分析Runtime Data Area(運行時數據區)的結構。

 

 1. 運行時數據區分爲幾個部分?

根據 JVM 規範,JVM 內存共分爲虛擬機棧、堆、方法區、程序計數器、本地方法棧五個部分。

 

名稱

特徵

作用

配置參數

異常

程序計數器

佔用內存小,線程私有,

生命週期與線程相同

大致爲字節碼行號指示器

虛擬機棧

線程私有,生命週期與線程相同,使用連續的內存空間

Java 方法執行的內存模型,存儲局部變量表、操作棧、動態鏈接、方法出口等信息

-Xss

StackOverflowError

OutOfMemoryError

java堆

線程共享,生命週期與虛擬機相同,可以不使用連續的內存地址

保存對象實例,所有對象實例(包括數組)都要在堆上分配

-Xms

-Xsx

-Xmn

OutOfMemoryError

方法區

線程共享,生命週期與虛擬機相同,可以不使用連續的內存地址

存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據

-XX:PermSize:

16M

-XX:MaxPermSize

64M

OutOfMemoryError

運行時常量池

方法區的一部分,具有動態性

存放字面量及符號引用

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1.1 方法區

方法區是java虛擬機規範去中定義的一種概念上的區域,具有什麼功能,但並沒有規定這個區域到底應該位於何處,因此對於實現者來說,如何來實際方法區是有着很大自由度的。

永生代是hotspot中的一個概念,其他jvm實現未必有,例如jrockit就沒這東西。java8之前,hotspot使用在內存中劃分出一塊區域來存儲類的元信息、類變量以及內部字符串(interned string)等內容,稱之爲永生代,把它作爲方法區來使用。

[JEP122][2]提議取消永生代,方法區作爲概念上的區域仍然存在。原先永生代中類的元信息會被放入本地內存(元數據區,metaspace),將類的靜態變量和內部字符串放入到java堆中。

爲了龍清楚方法區那麼需要解釋兩個名詞:永久代和元空間

PermGen(永久代)

絕大部分Java程序員應該都見過“java.lang.OutOfMemoryError: PremGen space”異常。這裏的“PermGen space”其實指的就是方法區。不過方法區和“PermGen space”又有着本質的區別。前者是JVM的規範,而後者則是JVM規範的一種實現,並且只有HotSpot纔有“PermGen space”,而對於其他類型的虛擬機,如JRockit(Oracle)、J9(IBM)並沒有“PermGen space”。由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出。並且JDK 1.8中參數PermSize和MaxPermSize已經失效。

元空間
其實,移除永久代的工作從JDK 1.7就開始了。JDK 1.7中,存儲在永久代的部分數據就已經轉移到Java Heap或者Native Heap。但永久代仍存在於JDK 1.7中,並沒有完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了Java heap;類的靜態變量(class statics)轉移到了Java heap。

JDK1.8對JVM架構的改造將類元數據放到本地內存中,另外,將常量池和靜態變量放到Java堆裏。HotSpot VM將會爲類的元數據明確分配和釋放本地內存。在這種架構下,類元信息就突破了原來-XX:MaxPermSize的限制,現在可以使用更多的本地內存。這樣就從一定程度上解決了原來在運行時生成大量類造成經常Full GC問題,如運行時使用反射、代理等。所以升級以後Java堆空間可能會增加。

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間的最大區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數指定元空間的大小:

-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對改值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。

-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

除了上面的兩個指定大小的選項外,還有兩個與GC相關的屬性:

-XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少爲分配空間所導致的垃圾收集。

-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少爲釋放空間所導致的垃圾收集。

 

所以對於方法區,Java8之後的變化:

  • 移除了永久代(PermGen),替換爲元空間(Metaspace);
  • 永久代中的 class metadata 轉移到了 native memory(本地內存,而不是虛擬機);
  • 永久代中的 interned Strings 和 class static variables 轉移到了 Java heap;
  • 永久代參數 (PermSize MaxPermSize) -> 元空間參數(MetaspaceSize MaxMetaspaceSize)

 

1.2 虛擬機棧(線程棧)與 堆(Heap)

爲更好的理解Java線程棧和堆,我們簡單的認爲Java內存模型把Java虛擬機內部劃分爲線程棧和堆。這張圖演示了Java內存模型的邏輯視圖。

Java Memory Model

每一個運行在Java虛擬機裏的線程都擁有自己的線程棧。這個線程棧包含了這個線程調用的方法當前執行點相關的信息。一個線程僅能訪問自己的線程棧。一個線程創建的本地變量對其它線程不可見,僅自己可見。即使兩個線程執行同樣的代碼,這兩個線程任然在在自己的線程棧中的代碼來創建本地變量。因此,每個線程擁有每個本地變量的獨有版本。

所有原始類型的本地變量都存放在線程棧上,因此對其它線程不可見。一個線程可能向另一個線程傳遞一個原始類型變量的拷貝,但是它不能共享這個原始類型變量自身。

堆上包含在Java程序中創建的所有對象,無論是哪一個對象創建的。這包括原始類型的對象版本。如果一個對象被創建然後賦值給一個局部變量,或者用來作爲另一個對象的成員變量,這個對象任然是存放在堆上。

下面這張圖演示了調用棧和本地變量存放在線程棧上,對象存放在堆上。

enter image description here

一個本地變量可能是原始類型,在這種情況下,它總是“呆在”線程棧上。

一個本地變量也可能是指向一個對象的一個引用。在這種情況下,引用(這個本地變量)存放在線程棧上,但是對象本身存放在堆上。

一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上,即使這些方法所屬的對象存放在堆上。

一個對象的成員變量可能隨着這個對象自身存放在堆上。不管這個成員變量是原始類型還是引用類型。

靜態成員變量跟隨着類定義一起也存放在堆上。

存放在堆上的對象可以被所有持有對這個對象引用的線程訪問。當一個線程可以訪問一個對象時,它也可以訪問這個對象的成員變量。如果兩個線程同時調用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,但是每一個線程都擁有這個本地變量的私有拷貝。

下圖演示了上面提到的點:

enter image description here

兩個線程擁有一些列的本地變量。其中一個本地變量(Local Variable 2)執行堆上的一個共享對象(Object 3)。這兩個線程分別擁有同一個對象的不同引用。這些引用都是本地變量,因此存放在各自線程的線程棧上。這兩個不同的引用指向堆上同一個對象。

注意,這個共享對象(Object 3)持有Object2和Object4一個引用作爲其成員變量(如圖中Object3指向Object2和Object4的箭頭)。通過在Object3中這些成員變量引用,這兩個線程就可以訪問Object2和Object4。

 

1.3 程序計數器

程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

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

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

此內存區域是唯一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

 

 1.4 本地方法棧

 

本地方法棧(Native MethodStacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。

 

與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

 

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