深刻理解JVM—JVM內存模型

深刻理解JVM—JVM內存模型

一線天色天宇星辰 IT哈哈
咱們知道,計算機CPU和內存的交互是最頻繁的,內存是咱們的高速緩存區,用戶磁盤和CPU的交互,而CPU運轉速度愈來愈快,磁盤遠遠跟不上CPU的讀寫速度,才設計了內存,用戶緩衝用戶IO等待致使CPU的等待成本,可是隨着CPU的發展,內存的讀寫速度也遠遠跟不上CPU的讀寫速度,所以,爲了解決這一糾紛,CPU廠商在每顆CPU上加入了高速緩存,用來緩解這種症狀,所以,如今CPU同內存交互就變成了下面的樣子。java

< xmlnamespace prefix ="v" ns ="urn:schemas-microsoft-com:vml" />< xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" />

深刻理解JVM—JVM內存模型

一樣,根據摩爾定律,咱們知道單核CPU的主頻不可能無限制的增加,要想不少的提高新能,須要多個處理器協同工做,Intel總裁的貝瑞特單膝下跪事件標誌着多核時代的到來。 程序員

深刻理解JVM—JVM內存模型
基於高速緩存的存儲交互很好的解決了處理器與內存之間的矛盾,也引入了新的問題:緩存一致性問題。在多處理器系統中,每一個處理器有本身的高速緩存,而他們又共享同一塊內存(下文成主存,main memory 主要內存),當多個處理器運算都涉及到同一塊內存區域的時候,就有可能發生緩存不一致的現象。爲了解決這一問題,須要各個處理器運行時都遵循一些協議,在運行時須要將這些協議保證數據的一致性。這類協議包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。以下圖所示
深刻理解JVM—JVM內存模型算法

Java虛擬機內存模型中定義的訪問操做與物理計算機處理的基本一致! 數組

深刻理解JVM—JVM內存模型

Java中經過多線程機制使得多個任務同時執行處理,全部的線程共享JVM內存區域main memory,而每一個線程又單獨的有本身的工做內存,當線程與內存區域進行交互時,數據從主存拷貝到工做內存,進而交由線程處理(操做碼+操做數)。更多信息咱們會在後面的《深刻JVM—JVM類執行機制中詳細解說》。 緩存

在以前,咱們也已經提到,JVM的邏輯內存模型以下:服務器

深刻理解JVM—JVM內存模型

咱們如今來逐個的看下每一個究竟是作什麼的!數據結構

一、程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它的做用能夠看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。多線程

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

若是線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Natvie 方法,這個計數器值則爲空(Undefined)。此內存區域是惟一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError 狀況的區域。jvm

二、Java 虛擬機棧

與程序計數器同樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java 方法執行的內存模型:每一個方法被執行的時候都會同時建立一個棧幀(Stack Frame ①)用於存儲局部變量表、操做棧、動態連接、方法出口等信息。每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
常常有人把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 異常。

三、本地方法棧

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

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

四、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 異常。

五、方法區

方法區(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 異常。

六、運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。

Java 虛擬機對Class 文件的每一部分(天然也包括常量池)的格式都有嚴格的規定,每個字節用於存儲哪一種數據都必須符合規範上的要求,這樣纔會被虛擬機承認、裝載和執行。但對於運行時常量池,Java 虛擬機規範沒有作任何細節的要求,不一樣的提供商實現的虛擬機能夠按照本身的須要來實現這個內存區域。不過,通常來講,除了保存Class 文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中①。

運行時常量池相對於Class 文件常量池的另一個重要特徵是具有動態性,Java 語言並不要求常量必定只能在編譯期產生,也就是並不是預置入Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是String 類的intern() 方法。

既然運行時常量池是方法區的一部分,天然會受到方法區內存的限制,當常量池沒法再申請到內存時會拋出OutOfMemoryError 異常

七、直接內存

直接內存(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 堆中的對象的具體位置,所以不一樣虛擬機實現的對象訪問方式會有所不一樣,主流的訪問方式有兩種:使用句柄和直接指針。

若是使用句柄訪問方式,Java 堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息,以下圖所示。
深刻理解JVM—JVM內存模型

若是使用直接指針訪問方式,Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,reference 中直接存儲的就是對象地址,以下圖所示

深刻理解JVM—JVM內存模型

這兩種對象的訪問方式各有優點,使用句柄訪問方式的最大好處就是reference 中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference 自己不須要被修改。

使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,因爲對象的訪問在Java 中很是頻繁,所以這類開銷聚沙成塔後也是一項很是可觀的執行成本。就本書討論的主要虛擬機Sun HotSpot 而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的範圍來看,各類語言和框架使用句柄來訪問的狀況也十分常見。

下面咱們來看幾個示例

一、Java 堆溢出

下面的程中咱們限制Java 堆的大小爲20MB,不可擴展(將堆的最小值-Xms 參
數與最大值-Xmx 參數設置爲同樣便可避免堆自動擴展),經過參數-XX:+HeapDump OnOutOfMemoryError 可讓虛擬機在出現內存溢出異常時Dump 出當前的內存堆轉儲快照以便過後進行分析。
參數設置以下
深刻理解JVM—JVM內存模型
深刻理解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
 * @author YHJ create at 2011-11-12 下午07:52:22
 * @FileNmae com.yhj.jvm.memory.heap.HeapOutOfMemory.java
 */
publicclass HeapOutOfMemory {
    /**
     * @param args
     * @Author YHJ create at 2011-11-12 下午07:52:18
     */
    publicstaticvoid main(String[] args) {
       List<TestCase> cases = new ArrayList<TestCase>();
       while(true){
           cases.add(new TestCase());
       }
    }
}
/**
 * @Described:測試用例
 * @author YHJ create at 2011-11-12 下午07:55:50
 * @FileNmae com.yhj.jvm.memory.heap.HeapOutOfMemory.java
 */
class TestCase{

}

深刻理解JVM—JVM內存模型
Java 堆內存的OutOfMemoryError異常是實際應用中最多見的內存溢出異常狀況。出現Java 堆內存溢出時,異常堆棧息「java.lang.OutOfMemoryError」會跟着進一步提示「Java heapspace」。

要解決這個區域的異常,通常的手段是首先經過內存映像分析工具(如Eclipse
Memory Analyzer)對dump 出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是要先分清楚究竟是出現了內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)。圖2-5 顯示了使用Eclipse Memory Analyzer 打開的堆轉儲快照文件。

若是是內存泄漏,可進一步經過工具查看泄漏對象到GC Roots 的引用鏈。因而就能找到泄漏對象是經過怎樣的路徑與GC Roots 相關聯並致使垃圾收集器沒法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots 引用鏈的信息,就能夠比較準確地定位出泄漏代碼的位置。

若是不存在泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx 與-Xms),與機器物理內存對比看是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。

以上是處理Java 堆內存問題的簡略思路,處理這些問題所須要的知識、工具與經驗在後面的幾回分享中我會作一些額外的分析。

二、java棧溢出

package com.yhj.jvm.memory.stack;
/**
 * @Described:棧層級不足探究
 * @VM args:-Xss128k
 * @author YHJ create at 2011-11-12 下午08:19:28
 * @FileNmae com.yhj.jvm.memory.stack.StackOverFlow.java
 */
publicclass StackOverFlow {
    privateinti ;
    publicvoid plus() {
       i++;
       plus();
    }
    /**
     * @param args
     * @Author YHJ create at 2011-11-12 下午08:19:21
     */
    publicstaticvoid 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();
       }
    }
}

三、常量池溢出(常量池都有哪些信息,咱們在後續的JVM類文件結構中詳細描述)

package com.yhj.jvm.memory.constant;
import java.util.ArrayList;
import java.util.List;
/**
 * @Described:常量池內存溢出探究
 * @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author YHJ create at 2011-10-30 下午04:28:30
 * @FileNmae com.yhj.jvm.memory.constant.ConstantOutOfMemory.java
 */
publicclass ConstantOutOfMemory { 
    /**
     * @param args
     * @throws Exception
     * @Author YHJ create at 2011-10-30 下午04:28:25
     */
    publicstaticvoid main(String[] args) throws Exception {
       try {
           List<String> strings = new ArrayList<String>();
           int i = 0;
           while(true){
              strings.add(String.valueOf(i++).intern());
           }
       } catch (Exception e) {
           e.printStackTrace();
           throw e;
       } 
    } 
}

四、方法去溢出

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
 * @author YHJ create at 2011-11-12 下午08:47:55
 * @FileNmae com.yhj.jvm.memory.methodArea.MethodAreaOutOfMemory.java
 */
publicclass MethodAreaOutOfMemory {
    /**
     * @param args
     * @Author YHJ create at 2011-11-12 下午08:47:51
     */
    publicstaticvoid 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:測試用例
 * @author YHJ create at 2011-11-12 下午08:53:09
 * @FileNmae com.yhj.jvm.memory.methodArea.MethodAreaOutOfMemory.java
 */
class TestCase{

}

五、直接內存溢出

package com.yhj.jvm.memory.directoryMemory;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
/**
 * @Described:直接內存溢出測試
 * @VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author YHJ create at 2011-11-12 下午09:06:10
 * @FileNmae com.yhj.jvm.memory.directoryMemory.DirectoryMemoryOutOfmemory.java
 */
publicclass DirectoryMemoryOutOfmemory {
    privatestaticfinalintONE_MB = 1024*1024;
    privatestaticintcount = 1; 
    /**
     * @param args
     * @Author YHJ create at 2011-11-12 下午09:05:54
     */
    publicstaticvoid 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();
       }
    } 
}
相關文章
相關標籤/搜索