天天看點

Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例

一、文章來由

開始實習啦,實習轉戰Java開發工程師。。。

二、JVM記憶體模型總圖

Java中通過多線程機制使得多個任務同時執行處理,所有的線程共享JVM記憶體區域main memory,而每個線程又單獨的有自己的工作記憶體,當線程與記憶體區域進行互動時,資料從主存拷貝到工作記憶體,進而交由線程處理(操作碼+操作數)。

在之前,我們也已經提到,JVM的邏輯記憶體模型如下:

Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例
Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例

三、JVM記憶體模型詳解

1、程式計數器

程式計數器(Program Counter Register)是一塊較小的記憶體空間,它的作用可以看

做是目前線程所執行的位元組碼的行号訓示器。在虛拟機的概念模型裡(僅是概念模型,

各種虛拟機可能會通過一些更高效的方式去實作),位元組碼解釋器工作時就是通過改變

這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、

線程恢複等基礎功能都需要依賴這個計數器來完成。

由于Java 虛拟機的多線程是通過線程輪流切換并配置設定處理器執行時間的方式來實作

的,在任何一個确定的時刻,一個處理器(對于多核處理器來說是一個核心)隻會執行

一條線程中的指令。是以,為了線程切換後能恢複到正确的執行位置,每條線程都需要

有一個獨立的程式計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類内

存區域為“線程私有”的記憶體。

如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛拟機位元組

碼指令的位址;如果正在執行的是Natvie 方法,這個計數器值則為空(Undefined)。此

記憶體區域是唯一一個在Java 虛拟機規範中沒有規定任何OutOfMemoryError 情況的區域。

2、Java 虛拟機棧

與程式計數器一樣,Java 虛拟機棧(Java Virtual Machine Stacks)也是線程私有的,

它的生命周期與線程相同。棧中主要存放一些基本類型的變量(,int, short, long, byte, float,

double, boolean, char)和對象句柄。 棧有一個很重要的特殊性,就是存在棧中的資料可以共享。

經常有人把Java 記憶體區分為堆記憶體(Heap)和棧記憶體(Stack),這種分法比較粗

糙,Java 記憶體區域的劃分實際上遠比這複雜。這種劃分方式的流行隻能說明大多數程式

員最關注的、與對象記憶體配置設定關系最密切的記憶體區域是這兩塊。所指的“棧”就是現在講的虛拟機

棧,或者說是虛拟機棧中的局部變量表部分。

局部變量表存放了編譯期可知的各種基本資料類型(boolean、byte、char、short、int、

float、long、double)、對象引用(reference 類型,它不等同于對象本身,根據不同的虛拟

機實作,它可能是一個指向對象起始位址的引用指針,也可能指向一個代表對象的句柄或

者其他與此對象相關的位置)和returnAddress 類型(指向了一條位元組碼指令的位址)。

其中64 位長度的long 和double 類型的資料會占用2 個局部變量空間(Slot),其餘

的資料類型隻占用1 個。局部變量表所需的記憶體空間在編譯期間完成配置設定,當進入一個

方法時,這個方法需要在幀中配置設定多大的局部變量空間是完全确定的,在方法運作期間

不會改變局部變量表的大小。

在Java 虛拟機規範中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError 異常;如果虛拟機棧可以動态擴充(目前大部分的Java 虛拟機都可動态擴充,隻不過Java 虛拟機規範中也允許固定長度的虛拟機棧),當擴充時無法申請到足夠的記憶體時會抛出OutOfMemoryError 異常。

3、本地方法棧

本地方法棧(Native Method Stacks)與虛拟機棧所發揮的作用是非常相似的,其

差別不過是虛拟機棧為虛拟機執行Java 方法(也就是位元組碼)服務,而本地方法棧則

是為虛拟機使用到的Native 方法服務。虛拟機規範中對本地方法棧中的方法使用的語

言、使用方式與資料結構并沒有強制規定,是以具體的虛拟機可以自由實作它。甚至

有的虛拟機(譬如Sun HotSpot 虛拟機)直接就把本地方法棧和虛拟機棧合二為一。

與虛拟機棧一樣,本地方法棧區域也會抛出StackOverflowError 和OutOfMemoryError

異常。

4、Java 堆

對于大多數應用來說,Java 堆(Java Heap)是Java 虛拟機所管理的記憶體中最大的

一塊。Java 堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。此記憶體區域的

唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體。這一點在Java 虛

拟機規範中的描述是:所有的對象執行個體以及數組都要在堆上配置設定①,但是随着JIT 編譯器

的發展與逃逸分析技術的逐漸成熟,棧上配置設定、标量替換②優化技術将會導緻一些微妙

的變化發生,所有的對象都配置設定在堆上也漸漸變得不是那麼“絕對”了。

Java 堆是垃圾收集器管理的主要區域,是以很多時候也被稱做“GC 堆”(Garbage

Collected Heap,幸好國内沒翻譯成“垃圾堆”)。如果從記憶體回收的角度看,由于現在

收集器基本都是采用的分代收集算法,是以Java 堆中還可以細分為:新生代和老年代;

再細緻一點的有Eden 空間、From Survivor 空間、To Survivor 空間等。如果從記憶體配置設定

的角度看,線程共享的Java 堆中可能劃分出多個線程私有的配置設定緩沖區(Thread Local

Allocation Buffer,TLAB)。不過,無論如何劃分,都與存放内容無關,無論哪個區域,

存儲的都仍然是對象執行個體,進一步劃分的目的是為了更好地回收記憶體,或者更快地配置設定

記憶體。在本章中,我們僅僅針對記憶體區域的作用進行讨論,Java 堆中的上述各個區域的

配置設定和回收等細節将會是下一章的主題。

根據Java 虛拟機規範的規定,Java 堆可以處于實體上不連續的記憶體空間中,隻要

邏輯上是連續的即可,就像我們的磁盤空間一樣。在實作時,既可以實作成固定大小

的,也可以是可擴充的,不過目前主流的虛拟機都是按照可擴充來實作的(通過-Xmx

和-Xms 控制)。如果在堆中沒有記憶體完成執行個體配置設定,并且堆也無法再擴充時,将會抛出

OutOfMemoryError 異常。

4、方法區

方法區(Method Area)與Java 堆一樣,是各個線程共享的記憶體區域,它用于存

儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。雖

然Java 虛拟機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個别名叫做Non-

Heap(非堆),目的應該是與Java 堆區分開來。

對于習慣在HotSpot 虛拟機上開發和部署程式的開發者來說,很多人願意把方法區

稱為“永久代”(Permanent Generation),本質上兩者并不等價,僅僅是因為HotSpot 虛

拟機的設計團隊選擇把GC 分代收集擴充至方法區,或者說使用永久代來實作方法區而

已。對于其他虛拟機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。即

使是HotSpot 虛拟機本身,根據官方釋出的路線圖資訊,現在也有放棄永久代并“搬家”

至Native Memory 來實作方法區的規劃了。

Java 虛拟機規範對這個區域的限制非常寬松,除了和Java 堆一樣不需要連續的内

存和可以選擇固定大小或者可擴充外,還可以選擇不實作垃圾收集。相對而言,垃圾

收集行為在這個區域是比較少出現的,但并非資料進入了方法區就如永久代的名字一

樣“永久”存在了。這個區域的記憶體回收目标主要是針對常量池的回收和對類型的卸

載,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是類型的解除安裝,條件

相當苛刻,但是這部分區域的回收确實是有必要的。在Sun 公司的BUG 清單中,曾出

現過的若幹個嚴重的BUG 就是由于低版本的HotSpot 虛拟機對此區域未完全回收而導

緻記憶體洩漏。

根據Java 虛拟機規範的規定,當方法區無法滿足記憶體配置設定需求時,将抛出

OutOfMemoryError 異常。

5、運作時常量池

運作時常量池(Runtime Constant Pool)是方法區的一部分。Class 檔案中除了有

類的版本、字段、方法、接口等描述等資訊外,還有一項資訊是常量池(Constant Pool

Table),用于存放編譯期生成的各種字面量和符号引用,這部分内容将在類加載後存放

到方法區的運作時常量池中。

Java 虛拟機對Class 檔案的每一部分(自然也包括常量池)的格式都有嚴格的規

定,每一個位元組用于存儲哪種資料都必須符合規範上的要求,這樣才會被虛拟機認可、

裝載和執行。但對于運作時常量池,Java 虛拟機規範沒有做任何細節的要求,不同的

提供商實作的虛拟機可以按照自己的需要來實作這個記憶體區域。不過,一般來說,除

了儲存Class 檔案中描述的符号引用外,還會把翻譯出來的直接引用也存儲在運作時常

量池中①。

運作時常量池相對于Class 檔案常量池的另外一個重要特征是具備動态性,Java 語

言并不要求常量一定隻能在編譯期産生,也就是并非預置入Class 檔案中常量池的内容

才能進入方法區運作時常量池,運作期間也可能将新的常量放入池中,這種特性被開發

人員利用得比較多的便是String 類的intern() 方法。

既然運作時常量池是方法區的一部分,自然會受到方法區記憶體的限制,當常量池無

法再申請到記憶體時會抛出OutOfMemoryError 異常

6、直接記憶體

直接記憶體(Direct Memory)并不是虛拟機運作時資料區的一部分,也不是Java

虛拟機規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導緻

OutOfMemoryError 異常出現,是以我們放到這裡一起講解。

在JDK 1.4 中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)

與緩沖區(Buffer)的I/O 方式,它可以使用Native 函數庫直接配置設定堆外記憶體,然

後通過一個存儲在Java 堆裡面的DirectByteBuffer 對象作為這塊記憶體的引用進行

操作。這樣能在一些場景中顯著提高性能,因為避免了在Java 堆和Native 堆中來

回複制資料。

顯然,本機直接記憶體的配置設定不會受到Java 堆大小的限制,但是,既然是記憶體,則

肯定還是會受到本機總記憶體(包括RAM 及SWAP 區或者分頁檔案)的大小及處理器

尋址空間的限制。伺服器管理者配置虛拟機參數時,一般會根據實際記憶體設定-Xmx

等參數資訊,但經常會忽略掉直接記憶體,使得各個記憶體區域的總和大于實體記憶體限制

(包括實體上的和作業系統級的限制),進而導緻動态擴充時出現OutOfMemoryError

異常。

邏輯記憶體模型我們已經看到了,那當我們建立一個對象的時候是怎麼進行通路的呢?

在Java 語言中,對象通路是如何進行的?對象通路在Java 語言中無處不在,是最普通的程式行為,但即使是最簡單的通路,也會卻涉及Java 棧、Java 堆、方法區這三個最重要記憶體區

域之間的關聯關系,如下面的這句代碼:

Object obj = new Object();
           

假設這句代碼出現在方法體中,那“Object obj”這部分的語義将會反映到Java 棧

的本地變量表中,作為一個reference 類型資料出現。而“new Object()”這部分的語義

将會反映到Java 堆中,形成一塊存儲了Object 類型所有執行個體資料值(Instance Data,對

象中各個執行個體字段的資料)的結構化記憶體,根據具體類型以及虛拟機實作的對象記憶體布

局(Object Memory Layout)的不同,這塊記憶體的長度是不固定的。另外,在Java 堆中

還必須包含能查找到此對象類型資料(如對象類型、父類、實作的接口、方法等)的地

址資訊,這些類型資料則存儲在方法區中。

由于reference 類型在Java 虛拟機規範裡面隻規定了一個指向對象的引用,并沒有

定義這個引用應該通過哪種方式去定位,以及通路到Java 堆中的對象的具體位置,是以

不同虛拟機實作的對象通路方式會有所不同,主流的通路方式有兩種:使用句柄和直接

指針。

(1)如果使用句柄通路方式,Java 堆中将會劃分出一塊記憶體來作為句柄池,reference

中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料和類型資料各自的

具體位址資訊,如下圖所示。

Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例

(2)如果使用直接指針通路方式,

Java 堆對象的布局中就必須考慮如何放置通路類型

資料的相關資訊,reference 中直接存儲的就是對象位址,如下圖所示

Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例

這兩種對象的通路方式各有優勢,使用句柄通路方式的最大好處就是

reference 中存

儲的是穩定的句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻

會改變句柄中的執行個體資料指針,而reference 本身不需要被修改。

使用直接指針通路方式的最大好處就是速度更快,它節省了一次指針定位的時間開

銷,由于對象的通路在Java 中非常頻繁,是以這類開銷積少成多後也是一項非常可觀的

執行成本。就本書讨論的主要虛拟機Sun HotSpot 而言,它是使用第二種方式進行對象通路的,但從整個軟體開發的範圍來看,各種語言和架構使用句柄來通路的情況也十分常見。

四、JVM記憶體模型示例

1、Java 堆溢出

下面的程中我們限制Java 堆的大小為20MB,不可擴充(将堆的最小值-Xms 參

數與最大值-Xmx 參數設定為一樣即可避免堆自動擴充),通過參數-XX:+HeapDump

OnOutOfMemoryError 可以讓虛拟機在出現記憶體溢出異常時Dump 出目前的記憶體堆轉儲

快照以便事後進行分析。

參數設定如下

Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例
Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例
Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例
package com.yhj.jvm.memory.heap;

import java.util.ArrayList;
import java.util.List;

/**

 * @Described:堆溢出測試

 * @VM args:-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails

 * @FileNmae com.yhj.jvm.memory.heap.HeapOutOfMemory.java

 */

public class HeapOutOfMemory {

    /**

     * @param args

     * @Author YHJ create at 2011-11-12 下午07:52:18

     */

    public static void main(String[] args) {

       List<TestCase> cases = new ArrayList<TestCase>();

       while(true){

           cases.add(new TestCase());

       }
    }

}

/**

 * @Described:測試用例

 * @FileNmae com.yhj.jvm.memory.heap.HeapOutOfMemory.java

 */

class TestCase{

}
           
Java之:JVM記憶體模型一、文章來由二、JVM記憶體模型總圖三、JVM記憶體模型詳解四、JVM記憶體模型示例

Java 堆記憶體的OutOfMemoryError異常是實際應用中最常見的記憶體溢出異常情況。出現Java 堆内

存溢出時,異常堆棧資訊“java.lang.OutOfMemoryError”會跟着進一步提示“Java heap

space”。

要解決這個區域的異常,一般的手段是首先通過記憶體映像分析工具(如Eclipse

Memory Analyzer)對dump 出來的堆轉儲快照進行分析,重點是确認記憶體中的對象是

否是必要的,也就是要先厘清楚到底是出現了記憶體洩漏(Memory Leak)還是記憶體溢

出(Memory Overflow)。圖2-5 顯示了使用Eclipse Memory Analyzer 打開的堆轉儲快

照檔案。

如果是記憶體洩漏,可進一步通過工具檢視洩漏對象到GC Roots 的引用鍊。于是就

能找到洩漏對象是通過怎樣的路徑與GC Roots 相關聯并導緻垃圾收集器無法自動回收

它們的。掌握了洩漏對象的類型資訊,以及GC Roots 引用鍊的資訊,就可以比較準确

地定位出洩漏代碼的位置。

如果不存在洩漏,換句話說就是記憶體中的對象确實都還必須存活着,那就應當檢查

虛拟機的堆參數(-Xmx 與-Xms),與機器實體記憶體對比看是否還可以調大,從代碼上

檢查是否存在某些對象生命周期過長、持有狀态時間過長的情況,嘗試減少程式運作期

的記憶體消耗。

2、Java棧溢出

package com.yhj.jvm.memory.stack;

/**

 * @Described:棧層級不足探究

 * @VM args:-Xss128k

 * @FileNmae com.yhj.jvm.memory.stack.StackOverFlow.java

 */

public class StackOverFlow {

    private int i ;

    public void plus() {

       i++;

       plus();

    }


    /**
     * @param args
     * 
     */

    public static void main(String[] args) {

       StackOverFlow stackOverFlow = new StackOverFlow();

       try {

           stackOverFlow.plus();

       } catch (Exception e) {

           System.out.println("Exception:stack length:"+stackOverFlow.i);

           e.printStackTrace();

       } catch (Error e) {

           System.out.println("Error:stack length:"+stackOverFlow.i);

           e.printStackTrace();

       }

    }

}
           

3、常量池溢出

package com.yhj.jvm.memory.constant;

import java.util.ArrayList;
import java.util.List;

/**

 * @Described:常量池記憶體溢出探究

 * @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M

 * @FileNmae com.yhj.jvm.memory.constant.ConstantOutOfMemory.java

 */

public class ConstantOutOfMemory {

    /**

     * @param args

     * @throws Exception

     */

    public static void main(String[] args) throws Exception {

       try {

           List<String> strings = new ArrayList<String>();

           int i = ;

           while(true){

              strings.add(String.valueOf(i++).intern());

           }

       } catch (Exception e) {

           e.printStackTrace();

           throw e;

       }
    }
}
           

4、方法區溢出

package com.yhj.jvm.memory.methodArea;

import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**

 * @Described:方法區溢出測試

 * 使用技術 CBlib

 * @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M

 * @FileNmae com.yhj.jvm.memory.methodArea.MethodAreaOutOfMemory.java

 */

public class MethodAreaOutOfMemory {



    /**

     * @param args

     */

    public static void main(String[] args) {

       while(true){

           Enhancer enhancer = new Enhancer();

           enhancer.setSuperclass(TestCase.class);

           enhancer.setUseCache(false);

           enhancer.setCallback(new MethodInterceptor() {

              @Override

              public Object intercept(Object arg0, Method arg1, Object[] arg2,

                     MethodProxy arg3) throws Throwable {

                  return arg3.invokeSuper(arg0, arg2);

              }

           });

           enhancer.create();

       }

    }

}

/**

 * @Described:測試用例

 * @FileNmae com.yhj.jvm.memory.methodArea.MethodAreaOutOfMemory.java

 */

class TestCase{

}
           

5、直接記憶體溢出

package com.yhj.jvm.memory.directoryMemory;

import java.lang.reflect.Field;
import sun.misc.Unsafe;

/**

 * @Described:直接記憶體溢出測試

 * @VM args: -Xmx20M -XX:MaxDirectMemorySize=10M

 * @FileNmae com.yhj.jvm.memory.directoryMemory.DirectoryMemoryOutOfmemory.java

 */

public class DirectoryMemoryOutOfmemory {

    private static final int ONE_MB = *;
    private static int count = ;

    /**

     * @param args

     */

    public static void main(String[] args) {

       try {

           Field field = Unsafe.class.getDeclaredField("theUnsafe");

           field.setAccessible(true);

           Unsafe unsafe = (Unsafe) field.get(null);

           while (true) {

              unsafe.allocateMemory(ONE_MB);

              count++;

           }

       } catch (Exception e) {

           System.out.println("Exception:instance created "+count);

           e.printStackTrace();

       } catch (Error e) {

           System.out.println("Error:instance created "+count);

           e.printStackTrace();

       }
    }
}
           

參考資料

[1] http://www.cnblogs.com/dingyingsi/p/3760447.html