1. 程式人生 > 其它 >Java Jvm記憶體區域定義介紹

Java Jvm記憶體區域定義介紹

本文主要用於個人筆記記錄,主要針對jdk1.8

一、Java記憶體區域(執行時數區)

圖片這X掉的是方法區,方法區是JVM的規範,大家可能會搞混永久代和方法區,其實永久代就是Jdk 1.8以前 HotSpot對方法區的實現。

 (圖片取自java guide)

直接記憶體是非執行時資料區的一部分。

Java 記憶體可以粗糙的區分為堆記憶體(Heap)和棧記憶體 (Stack)。棧記憶體大多指的是虛擬機器棧中區域性變量表部分。

 

二、執行緒私有記憶體區域

先看看執行緒內的記憶體區域:

1.程式計數器:用於記錄下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等功能都需要依賴這個計數器來完成。為了執行緒切換後能恢復到正確的執行位置,每個執行緒的程式計數器都是獨立的,這部分執行緒獨立的記憶體區域稱為執行緒私有。

注意:程式計數器是唯一一個不會出現 OutOfMemoryError 的記憶體區域,它的生命週期隨著執行緒的建立而建立,隨著執行緒的結束而死亡

2.Java 虛擬機器棧:生命週期和執行緒相同,描述的是 Java 方法執行的記憶體模型,每次方法呼叫的資料都是通過棧傳遞的。

    虛擬棧存著一個個棧幀,棧幀包括:區域性變量表、運算元棧、動態連結、方法出口資訊。

    區域性變量表,從名字顯而易見,是存放執行方法的時候變數的資訊,所以主要存放了編譯期可知的各種資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference 型別,它不同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制代碼或其他與此物件相關的位置)。

方法/函式的呼叫,就是建立相應函式的棧幀壓入Java棧,呼叫結束後,就會有棧幀彈出。

     虛擬機器棧主要是存放執行緒執行方法的一個個棧幀,棧幀佔用棧多少空間是根據具體方法的引數等資訊決定的,虛擬機器棧有可能出現StackOverFlowError 和 OutOfMemoryError

     每個執行緒的棧大小,通過-Xss設定,公司內預設為256k,相同實體記憶體下,-Xss小,則能生成更多的執行緒,但作業系統對一個程序的執行緒是有限制的。-Xss過小,會出現棧溢位,特別是有遞迴,大的迴圈的時候;-Xss過大,影響建立棧的數量,如果是多執行緒應用,會出現記憶體溢位。

  • StackOverFlowError
    : 若 Java 虛擬機器棧的記憶體大小不允許動態擴充套件(Hotspot不允許),那麼當執行緒請求棧的深度超過當前 Java 虛擬機器棧的最大深度的時候,就丟擲 StackOverFlowError 錯誤。
  • OutOfMemoryError: Java 虛擬機器棧的記憶體大小可以動態擴充套件(以前的Classic虛擬機器允許), 如果虛擬機器在動態擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。

3.本地方法棧

和虛擬機器棧所發揮的作用非常相似,區別是: 虛擬機器棧為虛擬機器執行 Java 方法 (也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的 Native (一些c、c++程式碼編寫的程式碼呼叫作業系統指令)方法服務。在 HotSpot 虛擬機器中本地方法棧和 Java 虛擬機器棧合二為一。

異常丟擲一樣會StackOverFlowError 和 OutOfMemoryError,棧幀也同樣是用於存放本地方法的區域性變量表、運算元棧、動態連結、出口資訊。

 

三、執行緒共享區域

1.堆

用於存放物件例項,“幾乎”所有的物件例項以及陣列都在這裡分配記憶體。

但是,隨著 JIT 編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,JDK1.7起預設開啟逃逸分析,如果某些方法中的物件引用沒有被返回或者未被外面使用(也就是未逃逸出去),那麼物件可以直接在棧上分配記憶體,就不會再分配到堆裡。

Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap)。

從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集演算法,所以 Java 堆還可以細分為:新生代和老年代;再細緻一點有:Eden、Survivor、Old 等空間。進一步劃分的目的是更好地回收記憶體,或者更快地分配記憶體。 

JDK 8後PermGen(永久代)已被 Metaspace(元空間) 取代,元空間使用的是直接記憶體。以前永久代用的是堆記憶體空間。

大部分情況,物件都會首先在 Eden 區域分配,在一次新生代垃圾回收後,如果物件還存活,則會進入 S0 或者 S1,並且物件的年齡還會加 1(Eden 區->Survivor 區後物件的初始年齡變為 1),當它的年齡增加到一定程度(預設為 15 歲)或者累積的某個年齡物件數量大小超過了 survivor 區的一半時,取這個年齡和 MaxTenuringThreshold 中更小的一個值,作為新的晉升年齡閾值;這些物件就會被晉升到老年代中。物件晉升到老年代的年齡閾值,通過引數 -XX:MaxTenuringThreshold 來設定。

物件流轉

物件年齡:0             取物件年齡超過一半的數和MaxTenuringThreshold最小值作為迭代到老年代的閾值並修改         物件年齡:MaxTenuringThreshold

Eden         ->             S0 或者 S1(看現在標記整理清除使用的是哪個新生代)                                                   -> 老年代(Tenured)

堆最容易出現OutOfMemoryError錯誤。但有好幾種:

 1.java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:當 JVM 花太多時間執行垃圾回收並且只能回收很少的堆空間時,就會發生此錯誤。

2.java.lang.OutOfMemoryError: Java heap space :假如在建立新的物件時, 堆記憶體中的空間不足以存放新建立的物件, 就會引發此錯誤。(和配置的最大堆記憶體有關,且受制於實體記憶體大小。最大堆記憶體可通過-Xmx引數配置。預設值看VM配置,-client取小於實體記憶體的 1/4 或 1GB,有的版本不受1GB影響;可以用這個命令檢視java -XX:+PrintFlagsFinal -version | grep -iE 'HeapSize|PermSize|ThreadStackSize', -XX:+PrintConmandLineFlags也可以但要看具體jvm。

 

2.方法區(元空間)

用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。注意:永久代就是 HotSpot 虛擬機器對虛擬機器規範中方法區的一種實現方式。而jdk1.8後是元空間。

jdk1.8後用這兩個命令設定元空間大小,與永久代很大的不同就是,如果不指定大小的話,隨著更多類的建立,虛擬機器會耗盡所有可用的系統記憶體。

-XX:MetaspaceSize=N //設定 Metaspace 的初始(和最小大小)

-XX:MaxMetaspaceSize=N //設定 Metaspace 的最大大小

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

《深入理解Java虛擬機器》第三版2.2.5提到

 

1.整個永久代有一個 JVM 本身設定的固定大小上限,無法進行調整,而元空間使用的是直接記憶體,受本機可用記憶體的限制,雖然元空間仍舊可能溢位,但是比原來出現的機率會更小。溢位時會報錯:java.lang.OutOfMemoryError: MetaSpace

2.可以使用 -XX:MaxMetaspaceSize 標誌設定最大元空間大小,預設值為 unlimited就意味著它只受系統記憶體的限制。-XX:MetaspaceSize 調整標誌定義元空間的初始大小如果未指定此標誌,則 Metaspace 將根據執行時的應用程式需求動態地重新調整大小。

3.在 JDK8,合併 HotSpot 和 JRockit 的程式碼時, JRockit 從來沒有一個叫永久代的東西, 合併之後就沒有必要額外的設定這麼一個永久代的地方了。 

 

3.執行時常量池

方法區一部分。存在方法區的類儲存(包括類的版本、欄位、方法、介面等描述資訊、常量池表)。存放編譯期生成的各種字面量和符號引用。

同樣因為在方法區中,當執行時常量池無法申請到記憶體時,會丟擲java.lang.OutOfMemoryError。

但字串常量放到了堆中。

JDK1.8版本的字串常量池中存的是字串物件,以及字串常量值。

幾個問題:

1.那麼JVM 常量池中儲存的是物件還是引用呢?字串常量池呢?

 執行時常量池其中的引用型別常量(例如CONSTANT_String、CONSTANT_Class、CONSTANT_MethodHandle、CONSTANT_MethodType之類)都存的是引用,實際的物件還是存在Java heap上的。字串常量池則看情況而定。如果使用StringBuilder.append先建立堆物件,後面再intern,則是把常量池中的物件指向堆物件,常量池中也是個引用,普通的情況,字串常量池是存了一個真的物件。

 

2.String s = new String("abc")這個語句建立了幾個物件?

建立了2個物件,第一個物件是”abc”字串儲存在常量池中,第二個物件在JAVA Heap中的 String 物件(物件儲存的就是字元常量池的引用),s指向的就是堆中這個String物件。

詳細見此文章:https://blog.csdn.net/wangwenjie1997/article/details/108325863

 

3.如果是用"+"拼接的字串呢?

"+"會被替換為採用了StringBuilder進行加號的拼接,只會在堆中建立一個String物件,並不會在常量池中儲存對應的字串。而“+”建立完字串後,再呼叫intern做池化,因為JDK7之後對字串常量池放到了堆中,所以當intern呼叫時,如果常量池沒有這個字串,就會在常量池中存了堆中對應字串的引用。

即:當字串常量池中並不存在對應字串時,呼叫intern方法返回的地址為堆中物件的地址。

 

4.String.intern()方法流程?

String類的intern()方法:一個初始為空的字串池,它由類String獨自維護。當呼叫 intern方法時,如果字串常量池池已經包含一個等於此String物件的字串(用equals(oject)方法確定),則返回池中的字串。否則,將此String物件新增到池中,並返回此String物件(注意是常量池中的物件,不是堆中的物件)的引用。 對於任意兩個字串s和t,當且僅當s.equals(t)為true時,s.intern() == t.intern()才為true。所有字面值字串和字串賦值常量表達式都使用 intern方法進行操作。 1.7後使用intern不同的地方是,如果存在堆中的物件,字元常量池會直接儲存這個堆中物件的引用,而不會重新建立物件。 詳細的String.intern作用描述可以看該文章https://www.cnblogs.com/yrjns/p/12507892.html。  

5.那麼的String.intern如何做到節省記憶體呢?

比如一個10w次的迴圈,new String(String.valueOf(sample[i % sample.length])),迴圈中如果字串有重複的時候,new String建立都會判斷常量池是否有該字面量物件,如果沒有,都會建立一個,再建立一個堆中的字串物件;也就是一直會在堆中建立一個物件指向字串常量池的字串物件。但如果用了new String(String.valueOf(sample[i % sample.length])).intern();new String().intern實際上如果字串常量池有這個字串物件,就會直接返回常量池的物件引用給到變數,這個過程是不會在 Java 堆中再建立一個 String 物件的。

 

4.直接記憶體

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。也可能導致 OutOfMemoryError 錯誤出現。

JDK1.4 中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel)與快取區(Buffer)的 I/O 方式,它可以直接使用 Native 函式庫直接分配堆外記憶體,然後通過一個儲存在 Java 堆中的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。這樣就能在一些場景中顯著提高效能,因為避免了在 Java 堆和 Native 堆之間來回複製資料。本機直接記憶體的分配不會受到 Java 堆的限制,但是,是記憶體就會受到本機總記憶體大小以及處理器定址空間的限制。