JVM內存機制

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

 

 一樣,根據摩爾定律,咱們知道單核html

CPU的主頻不可能無限制的增加,要想不少的提高新能,須要多個處理器協同工做,  Intel總裁的貝瑞特單膝下跪事件標誌着多核時代的到來。

 

 

 基於高速緩存的存儲交互很好的解決了處理器與內存之間的矛盾,也引入了新的問題:緩存一致性問題。在多處理器系統中,每一個處理器有本身的高速緩存,而他們又共享同一塊內存(下文成主存,java

main memory 主要內存),當多個處理器運算都涉及到同一塊內存區域的時候,就有可能發生緩存不一致的現象。爲了解決這一問題,須要各個處理器運行時都遵循一些協議,在運行時須要將這些協議保證數據的一致性。這類協議包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。以下圖所示

 

 

 Java算法

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

 

 

 Java數組

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

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

 

 

 咱們如今來逐個的看下每一個究竟是作什麼的!服務器

一、程序計數器數據結構

程序計數器(Program Counter Register)是一塊較小的內存空間,它的做用能夠看多線程

作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,框架

各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就是經過改變jvm

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

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

因爲Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現

的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)只會執行

一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要

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

存區域爲「線程私有」的內存。

若是線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節

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

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

二、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

中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的

具體地址信息,以下圖所示。

 

 

 若是使用直接指針訪問方式,

Java 堆對象的佈局中就必須考慮如何放置訪問類型

數據的相關信息,reference 中直接存儲的就是對象地址,以下圖所示

 

 

 這兩種對象的訪問方式各有優點,使用句柄訪問方式的最大好處就是

reference 中存

儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只

會改變句柄中的實例數據指針,而reference 自己不須要被修改。

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

銷,因爲對象的訪問在Java 中很是頻繁,所以這類開銷聚沙成塔後也是一項很是可觀的

執行成本。就本書討論的主要虛擬機Sun HotSpot 而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的範圍來看,各類語言和框架使用句柄來訪問的狀況也十分常見。

下面咱們來看幾個示例

一、Java 堆溢出

下面的程中咱們限制Java 堆的大小爲20MB,不可擴展(將堆的最小值-Xms 參

數與最大值-Xmx 參數設置爲同樣便可避免堆自動擴展),經過參數-XX:+HeapDump

OnOutOfMemoryError 可讓虛擬機在出現內存溢出異常時Dump 出當前的內存堆轉儲

快照以便過後進行分析。

參數設置以下

 
 

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

 */

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:測試用例

 * @author YHJ create at 2011-11-12 下午07:55:50

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

 */

class TestCase{

   

}

 

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),與機器物理內存對比看是否還能夠調大,從代碼上

檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期

的內存消耗。

以上是處理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

 */

public class StackOverFlow {

   

   

    private int i ;

   

    public void plus() {

       i++;

       plus();

    }

 

    /**

     * @param args

     * @Author YHJ create at 2011-11-12 下午08:19:21

     */

    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();

       }

 

    }

 

}

三、常量池溢出(常量池都有哪些信息,咱們在後續的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

 */

public class ConstantOutOfMemory {

 

    /**

     * @param args

     * @throws Exception

     * @Author YHJ create at 2011-10-30 下午04:28:25

     */

    public static void 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

 */

public class MethodAreaOutOfMemory {

 

    /**

     * @param args

     * @Author YHJ create at 2011-11-12 下午08:47:51

     */

    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:測試用例

 * @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

 */

public class DirectoryMemoryOutOfmemory {

 

    private static final int ONE_MB = 1024*1024;

    private static int count = 1;

 

    /**

     * @param args

     * @Author YHJ create at 2011-11-12 下午09:05:54

     */

    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();

       }

 

    }

 

}