1. 程式人生 > >JVM執行時資料區域劃分

JVM執行時資料區域劃分

前言

我們知道,計算機CPU和記憶體的互動是最頻繁的,記憶體是我們的快取記憶體區,使用者磁碟和CPU的互動,而CPU運轉速度越來越快,磁碟遠遠跟不上CPU的讀寫速度,才設計了記憶體,使用者緩衝使用者IO等待導致CPU的等待成本,但是隨著CPU的發展,記憶體的讀寫速度也遠遠跟不上CPU的讀寫速度,因此,為了解決這一糾紛,CPU廠商在每顆CPU上加入了快取記憶體,用來緩解這種症狀,因此,現在CPU同記憶體互動就變成了下面的樣子。

同樣,根據摩爾定律,我們知道單核CPU的主頻不可能無限制的增長,要想很多的提升新能,需要多個處理器協同工作, Intel總裁的貝瑞特單膝下跪事件標誌著多核時代的到來。

 

 基於快取記憶體的儲存互動很好的解決了處理器與記憶體之間的矛盾,也引入了新的問題:快取一致性問題。在多處理器系統中,每個處理器有自己的快取記憶體,而他們又共享同一塊記憶體(下文成主存,main memory 主要記憶體),當多個處理器運算都涉及到同一塊記憶體區域的時候,就有可能發生快取不一致的現象。為了解決這一問題,需要各個處理器執行時都遵循一些協議,在執行時需要將這些協議保證資料的一致性。這類協議包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。如下圖所示

 

什麼是JVM

JVM是Java Virtual Machine(Java虛擬機器)的縮寫,JVM是一種用於計算裝置的規範,它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。Java虛擬機器包括一套位元組碼指令集、一組暫存器、一個棧、一個垃圾回收堆和一個儲存方法域。 

JRE/JDK/JVM是什麼關係

JRE(JavaRuntimeEnvironment,Java執行環境),也就是Java平臺。所有的Java 程式都要在JRE下才能執行。普通使用者只需要執行已開發好的java程式,安裝JRE即可。

JDK(Java Development Kit)是程式開發者用來來編譯、除錯java程式用的開發工具包。JDK的工具也是Java程式,也需要JRE才能執行。為了保持JDK的獨立性和完整性,在JDK的安裝過程中,JRE也是安裝的一部分。所以,在JDK的安裝目錄下有一個名為jre的目錄,用於存放JRE檔案。

JVM(JavaVirtualMachine,Java虛擬機器)是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。JVM有自己完善的硬體架構,如處理器、堆疊、暫存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平臺執行。使用JVM就是為了支援與作業系統無關,實現跨平臺。

JVM執行程式的過程

1) 載入.class檔案

2) 管理並分配記憶體

3) 執行垃圾收集

JRE(java執行時環境)是JVM構造的java程式的執行環境,也是Java程式執行的環境,但是一個作業系統的一個應用程式一個程序也有他自己的執行的生命週期,也有自己的程式碼和資料空間。JVM在整個jdk中處於最底層,負責於作業系統的互動,用來遮蔽作業系統環境,提供一個完整的Java執行環境,因此也就虛擬計算機。作業系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境:

1) 建立JVM裝載環境和配置

2) 裝載JVM.dll

3) 初始化JVM.dll並掛界到JNIENV(JNI呼叫介面)例項

4) 呼叫JNIEnv例項裝載並處理class類。

JVM的生命週期

1) JVM例項對應了一個獨立執行的java程式它是程序級別 
a) 啟動。啟動一個Java程式時,一個JVM例項就產生了,任何一個擁有public static void main(String[] args)函式的class都可以作為JVM例項執行的起點 
b) 執行。main()作為該程式初始執行緒的起點,任何其他執行緒均由該執行緒啟動。JVM內部有兩種執行緒:守護執行緒和非守護執行緒,main()屬於非守護執行緒,守護執行緒通常由JVM自己使用,java程式也可以表明自己建立的執行緒是守護執行緒 
c) 消亡。當程式中的所有非守護執行緒都終止時,JVM才退出;若安全管理器允許,程式也可以使用Runtime類或者System.exit()來退出

2) JVM執行引擎例項則對應了屬於使用者執行程式的執行緒它是執行緒級別的

JVM垃圾回收

GC (Garbage Collection)的基本原理:將記憶體中不再被使用的物件進行回收,GC中用於回收的方法稱為收集器,由於GC需要消耗一些資源和時間,Java在對物件的生命週期特徵進行分析後,按照新生代、舊生代的方式來對物件進行收集,以儘可能的縮短GC對應用造成的暫停
(1)對新生代的物件的收集稱為minor GC;
(2)對舊生代的物件的收集稱為Full GC;
(3)程式中主動呼叫System.gc()強制執行的GC為Full GC。
不同的物件引用型別, GC會採用不同的方法進行回收,JVM物件的引用分為了四種類型:
(1)強引用:預設情況下,物件採用的均為強引用(這個物件的例項沒有其他物件引用,GC時才會被回收)
(2)軟引用:軟引用是Java中提供的一種比較適合於快取場景的應用(只有在記憶體不夠用的情況下才會被GC)
(3)弱引用:在GC時一定會被GC回收
(4)虛引用:由於虛引用只是用來得知物件是否被GC

JVM的記憶體區域劃分

  學過C語言的朋友都知道C編譯器在劃分記憶體區域的時候經常將管理的區域劃分為資料段和程式碼段,資料段包括堆、棧以及靜態資料區。那麼在Java語言當中,記憶體又是如何劃分的呢?

  由於Java程式是交由JVM執行的,所以我們在談Java記憶體區域劃分的時候事實上是指JVM記憶體區域劃分。在討論JVM記憶體區域劃分之前,先來看一下Java程式具體執行的過程:

 

  如上圖所示,首先Java原始碼檔案(.java字尾)會被Java編譯器編譯為位元組碼檔案(.class字尾),然後由JVM中的類載入器載入各個類的位元組碼檔案,載入完畢之後,交由JVM執行引擎執行。在整個程式執行過程中,JVM會用一段空間來儲存程式執行期間需要用到的資料和相關資訊,這段空間一般被稱作為Runtime Data Area(執行時資料區),也就是我們常說的JVM記憶體。因此,在Java中我們常常說到的記憶體管理就是針對這段空間進行管理(如何分配和回收記憶體空間)。

  在知道了JVM記憶體是什麼東西之後,下面我們就來討論一下這段空間具體是如何劃分區域的,是不是也像C語言中一樣也存在棧和堆呢?

一.執行時資料區包括哪幾部分?

  根據《Java虛擬機器規範》的規定,執行時資料區通常包括這幾個部分:程式計數器(Program Counter Register)、Java棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(Method Area)、堆(Heap)。

如上圖所示,JVM中的執行時資料區應該包括這些部分。在JVM規範中雖然規定了程式在執行期間執行時資料區應該包括這幾部分,但是至於具體如何實現並沒有做出規定,不同的虛擬機器廠商可以有不同的實現方式。

二.執行時資料區的每部分到底儲存了哪些資料?

  下面我們來了解一下執行時資料區的每部分具體用來儲存程式執行過程中的哪些資料。

1.程式計數器

  程式計數器(Program Counter Register),也有稱作為PC暫存器。想必學過組合語言的朋友對程式計數器這個概念並不陌生,在組合語言中,程式計數器是指CPU中的暫存器,它儲存的是程式當前執行的指令的地址(也可以說儲存下一條指令的所在儲存單元的地址),當CPU需要執行指令時,需要從程式計數器中得到當前需要執行的指令所在儲存單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程式計數器便自動加1或者根據轉移指標得到下一條指令的地址,如此迴圈,直至執行完所有的指令。

  雖然JVM中的程式計數器並不像組合語言中的程式計數器一樣是物理概念上的CPU暫存器,但是JVM中的程式計數器的功能跟組合語言中的程式計數器的功能在邏輯上是等同的,也就是說是用來指示 執行哪條指令的。

  由於在JVM中,多執行緒是通過執行緒輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的核心只會執行一條執行緒中的指令,因此,為了能夠使得每個執行緒都線上程切換後能夠恢復在切換之前的程式執行位置,每個執行緒都需要有自己獨立的程式計數器,並且不能互相被幹擾,否則就會影響到程式的正常執行次序。因此,可以這麼說,程式計數器是每個執行緒所私有的。

  在JVM規範中規定,如果執行緒執行的是非native方法,則程式計數器中儲存的是當前需要執行的指令的地址;如果執行緒執行的是native方法,則程式計數器中的值是undefined。

  由於程式計數器中儲存的資料所佔空間的大小不會隨程式的執行而發生改變,因此,對於程式計數器是不會發生記憶體溢位現象(OutOfMemory)的。

2.Java棧

  Java棧也稱作虛擬機器棧(Java Vitual Machine Stack),也就是我們常常所說的棧,跟C語言的資料段中的棧類似。事實上,Java棧是Java方法執行的記憶體模型。為什麼這麼說呢?下面就來解釋一下其中的原因。

  Java棧中存放的是一個個的棧幀,每個棧幀對應一個被呼叫的方法,在棧幀中包括區域性變量表(Local Variables)、運算元棧(Operand Stack)、指向當前方法所屬的類的執行時常量池(執行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加資訊。當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。因此可知,執行緒當前執行的方法所對應的棧幀必定位於Java棧的頂部。講到這裡,大家就應該會明白為什麼 在 使用 遞迴方法的時候容易導致棧記憶體溢位的現象了以及為什麼棧區的空間不用程式設計師去管理了(當然在Java中,程式設計師基本不用關係到記憶體分配和釋放的事情,因為Java有自己的垃圾回收機制),這部分空間的分配和釋放都是由系統自動實施的。對於所有的程式設計語言來說,棧這部分空間對程式設計師來說是不透明的。下圖表示了一個Java棧的模型:

  區域性變量表,顧名思義,想必不用解釋大家應該明白它的作用了吧。就是用來儲存方法中的區域性變數(包括在方法中宣告的非靜態變數以及函式形參)。對於基本資料型別的變數,則直接儲存它的值,對於引用型別的變數,則存的是指向物件的引用。區域性變量表的大小在編譯器就可以確定其大小了,因此在程式執行期間區域性變量表的大小是不會改變的。

  運算元棧,想必學過資料結構中的棧的朋友想必對錶達式求值問題不會陌生,棧最典型的一個應用就是用來對錶達式求值。想想一個執行緒執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麼說,程式中的所有計算過程都是在藉助於運算元棧來完成的。

  指向執行時常量池的引用,因為在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向執行時常量。

  方法返回地址,當一個方法執行完畢之後,要返回之前呼叫它的地方,因此在棧幀中必須儲存一個方法返回地址。

  由於每個執行緒正在執行的方法可能不同,因此每個執行緒都會有一個自己的Java棧,互不干擾。

3.本地方法棧

  本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的。在JVM規範中,並沒有對本地方發展的具體實現方法以及資料結構作強制規定,虛擬機器可以自由實現它。在HotSopt虛擬機器中直接就把本地方法棧和Java棧合二為一

4.堆

  在C語言中,堆這部分空間是唯一一個程式設計師可以管理的記憶體區域。程式設計師可以通過malloc函式和free函式在堆上申請和釋放空間。那麼在Java中是怎麼樣的呢?

  Java中的堆是用來儲存物件本身的以及陣列(當然,陣列引用是存放在Java棧中的)。只不過和C語言中的不同,在Java中,程式設計師基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理。因此這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被所有執行緒共享的,在JVM中只有一個堆。

上圖引自網路,但有個問題:方法區和heap堆都是執行緒共享的記憶體區域。

堆空間記憶體分配(預設情況下)
老年代 : 三分之二的堆空間
年輕代 : 三分之一的堆空間 
eden區: 8/10 的年輕代空間
From survivor : 1/10 的年輕代空間
To survivor : 1/10 的年輕代空間
命令列上執行如下命令,檢視所有預設的jvm引數
java -XX:+PrintFlagsFinal -version

-XX:InitialSurvivorRatio    新生代Eden/Survivor空間的初始比例
-XX:Newratio    Old區 和 Yong區 的記憶體比例

一道推算題:預設引數下,如果僅給出eden區40M,求堆空間總大小
根據比例可以推算出,兩個survivor區各5M,年輕代50M。老年代是年輕代的兩倍,即100M。那麼堆總大小就是150M

關於方法區和永久代:

在HotSpot JVM中,這次討論的永久代,就是上圖的方法區(JVM規範中稱為方法區)。《Java虛擬機器規範》只是規定了有方法區這麼個概念和它的作用,並沒有規定如何去實現它。在其他JVM上不存在永久代。

1.7構成

1.8的構成

堆是JVM記憶體佔用最大,管理最複雜的一個區域。其唯一的用途就是存放物件例項:所有的物件例項及陣列都在對上進行分配。1.7後,字串常量池從永久代中剝離出來,存放在堆中。堆有自己進一步的記憶體分塊劃分,按照GC分代收集角度的劃分請參見上圖。

(1) 堆是JVM中所有執行緒共享的,因此在其上進行物件記憶體的分配均需要進行加鎖,這也導致了new物件的開銷是比較大的
(2) Sun Hotspot JVM為了提升物件記憶體分配的效率,對於所建立的執行緒都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據執行的情況計算而得,在TLAB上分配物件時不需要加鎖,因此JVM在給執行緒的物件分配記憶體時會盡量的在TLAB上分配,在這種情況下JVM中分配物件記憶體的效能和C基本是一樣高效的,但如果物件過大的話則仍然是直接使用堆空間分配
(3) TLAB僅作用於新生代的Eden Space,因此在編寫Java程式時,通常多個小的物件比大的物件分配起來更加高效。
(4) 所有新建立的Object 都將會儲存在新生代Yong Generation中。如果Young Generation的資料在一次或多次GC後存活下來,那麼將被轉移到OldGeneration。新的Object總是建立在Eden Space。

1.新生代:Eden+From Survivor+To Survivor

2.老年代:OldGen

3.永久代(方法區的實現) : PermGen----->替換為Metaspace(本地記憶體中)

5.方法區

  方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被執行緒共享的區域。在方法區中,儲存了每個類的資訊(包括類的名稱、方法資訊、欄位資訊)、靜態變數、常量以及編譯器編譯後的程式碼等。方法區邏輯上屬於堆的一部分,但是為了與堆進行區分,通常又叫“非堆”。

  在Class檔案中除了類的欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用來儲存編譯期間生成的字面量和符號引用。

  在方法區中有一個非常重要的部分就是執行時常量池,它是每一個類或介面的常量池的執行時表示形式,在類和介面被載入到JVM後,對應的執行時常量池就被創建出來。當然並非Class檔案常量池中的內容才能進入執行時常量池,在執行期間也可將新的常量放入執行時常量池中,比如String的intern方法。

  在JVM規範中,沒有強制要求方法區必須實現垃圾回收。很多人習慣將方法區稱為“永久代”,是因為HotSpot虛擬機器以永久代來實現方法區,從而JVM的垃圾收集器可以像管理堆區一樣管理這部分割槽域,從而不需要專門為這部分設計垃圾回收機制。不過自從JDK7之後,Hotspot虛擬機器便將執行時常量池從永久代移除了

永久代(PermGen) ,絕大部分 Java 程式設計師應該都見過 “java.lang.OutOfMemoryError: PermGen space “這個異常。這裡的 “PermGen space”其實指的就是方法區。不過方法區和“PermGen space”又有著本質的區別。前者是 JVM 的規範,而後者則是 JVM 規範的一種實現,並且只有 HotSpot 才有 “PermGen space”,而對於其他型別的虛擬機器,如 JRockit(Oracle)、J9(IBM) 並沒有“PermGen space”。由於方法區主要儲存類的相關資訊,所以對於動態生成類的情況比較容易出現永久代的記憶體溢位。最典型的場景就是,在 jsp 頁面比較多的情況,容易出現永久代記憶體溢位。

6.元空間(Metaspace)

元空間的記憶體大小

元空間是方法區的在HotSpot jvm 中的實現,方法區主要用於儲存類的資訊、常量池、方法資料、方法程式碼等。方法區邏輯上屬於堆的一部分,但是為了與堆進行區分,通常又叫“非堆”。

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。,理論上取決於32位/64位系統可虛擬的記憶體大小。可見也不是無限制的,需要配置引數。

常用配置引數

1.MetaspaceSize

初始化的Metaspace大小,控制元空間發生GC的閾值。GC後,動態增加或降低MetaspaceSize。在預設情況下,這個值大小根據不同的平臺在12M到20M浮動。使用Java -XX:+PrintFlagsInitial命令檢視本機的初始化引數。

2.MaxMetaspaceSize

限制Metaspace增長的上限,防止因為某些情況導致Metaspace無限的使用本地記憶體,影響到其他程式。在本機上該引數的預設值為4294967295B(大約4096MB)。

3.MinMetaspaceFreeRatio

當進行過Metaspace GC之後,會計算當前Metaspace的空閒空間比,如果空閒比小於這個引數(即實際非空閒佔比過大,記憶體不夠用),那麼虛擬機器將增長Metaspace的大小。預設值為40,也就是40%。設定該引數可以控制Metaspace的增長的速度,太小的值會導致Metaspace增長的緩慢,Metaspace的使用逐漸趨於飽和,可能會影響之後類的載入。而太大的值會導致Metaspace增長的過快,浪費記憶體。

4.MaxMetasaceFreeRatio

當進行過Metaspace GC之後, 會計算當前Metaspace的空閒空間比,如果空閒比大於這個引數,那麼虛擬機器會釋放Metaspace的部分空間。預設值為70,也就是70%。

5.MaxMetaspaceExpansion

Metaspace增長時的最大幅度。在本機上該引數的預設值為5452592B(大約為5MB)。

6.MinMetaspaceExpansion

Metaspace增長時的最小幅度。在本機上該引數的預設值為340784B(大約330KB為)。

測試並追蹤元空間大小

測試字串常量

public class StringOomMock {
    static String  base = "string";
    
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i=0;i< Integer.MAX_VALUE;i++){
            String str = base + base;
            base = str;
            list.add(str.intern());
        }
    }
}

在eclipse中選中類--》run configuration-->java application--》new 引數如下:

 由於設定了最大記憶體20M,很快就溢位,如下圖:

 可見在jdk8中:

1.字串常量由永久代轉移到堆中。

2.持久代已不存在,PermSize MaxPermSize引數已移除。(看圖中最後兩行)

測試元空間溢位

根據定義,我們以載入類來測試元空間溢位,程式碼如下:

package jdk8;

import java.io.File;
import java.lang.management.ClassLoadingMXBean;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

/**
 * 
 * @ClassName:OOMTest
 * @Description:模擬類載入溢位(元空間oom)
 * @author diandian.zhang
 * @date 2017年4月27日上午9:45:40
 */
public class OOMTest {  
    public static void main(String[] args) {  
        try {  
            //準備url  
            URL url = new File("D:/58workplace/11study/src/main/java/jdk8").toURI().toURL();  
            URL[] urls = {url};  
            //獲取有關型別載入的JMX介面  
            ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();  
            //用於快取類載入器  
            List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();  
            while (true) {  
                //載入型別並快取類載入器例項  
                ClassLoader classLoader = new URLClassLoader(urls);  
                classLoaders.add(classLoader);  
                classLoader.loadClass("ClassA");  
                //顯示數量資訊(共載入過的型別數目,當前還有效的型別數目,已經被解除安裝的型別數目)  
                System.out.println("total: " + loadingBean.getTotalLoadedClassCount());  
                System.out.println("active: " + loadingBean.getLoadedClassCount());  
                System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

為了快速溢位,設定引數:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=80m,執行結果如下:

 上圖證實了,我們的JDK8中類載入(方法區的功能)已經不在永久代PerGem中了,而是Metaspace中。可以配合JVisualVM來看,更直觀一些。