程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 進修JVM之java內存區域與異常

進修JVM之java內存區域與異常

編輯:關於JAVA

進修JVM之java內存區域與異常。本站提示廣大學習愛好者:(進修JVM之java內存區域與異常)文章只能為提供參考,不一定能成為您想要的結果。以下是進修JVM之java內存區域與異常正文


1、媒介

java是一門跨硬件平台的面向對象高等編程說話,java法式運轉在java虛擬機上(JVM),由JVM治理內存,這點是和C++最年夜差別;固然內存有JVM治理,然則我們也必需要懂得JVM是若何治理內存的;JVM不是只要一種,以後存在的虛擬機能夠達幾十款,然則一個相符標准的虛擬機設計是必需遵守《java 虛擬機標准》的,本文是基於HotSpot虛擬機描寫,關於和其它虛擬機有差別會提到;本文重要描寫JVM中內存是若何散布、java法式的對象是若何存儲拜訪、各個內存區域能夠湧現的異常。

2、JVM中內存散布(區域)

JVM在履行java法式的時會把內存分為多個分歧的數據區域停止治理,這些區域有著紛歧樣的感化、創立和燒毀時光,有的區域是在JVM過程啟動時分派,有的區域則與用戶線程(法式自己的線程)的性命周期相干;依照JVM標准,JVM治理的內存區域分為以下幾個運轉時數據區域:


1、虛擬機棧

這塊內存區域是線程公有的,隨線程啟動而創立、線程燒毀而燒毀;虛擬機棧描寫的java辦法履行的內存模子:每一個辦法在履行開端會創立一個棧幀(Stack Frame),用於存儲部分變量表、操作數棧,靜態鏈接、辦法出口等。每一個辦法的挪用履行和前往停止,都對應有一個棧幀在虛擬機棧入棧和出棧的進程。

部分變量表望文生義是存儲部分變量的內存區域:寄存編譯器期可知的根本數據類型(8種java根本數據類型)、援用類型、前往地址;個中占64位的long和double類型數據會占用2個部分變量空間,其它數據類型只占用1個;因為類型年夜小肯定、變量數目編譯期可知,所以部分變量表在創立時是已知年夜小,這部門內存空間能在編譯期完成份配,而且在辦法運轉時代不須要修正部分變量表年夜小。

在虛擬機標准中,對這塊內存區域劃定了兩種異常:

1.假如線程要求的棧深度年夜於虛擬機所許可的深度(?),將拋出StackOverflowError異常;

2.假如虛擬機可以靜態擴大,當擴大是沒法請求到足夠內存,將拋出OutOfMemory異常;

2、當地辦法棧

當地辦法棧異樣也是線程公有,並且和虛擬機棧感化簡直是一樣的:虛擬機棧是為java辦法履行供給收支棧辦事,而當地辦法棧則是為虛擬機履行Native辦法供給辦事。

在虛擬機標准中,對當地辦法棧完成方法沒有強迫劃定,可以由詳細虛擬機自在完成;HotSpot虛擬機是直接把虛擬機棧和當地辦法棧合二為一完成;關於其他虛擬機完成這一塊的辦法,讀者有興致可以自行查詢相干材料;

與虛擬機棧一樣,當地辦法棧異樣會拋出StackOverflowError和OutOfMemory異常。

3、法式盤算器

法式器盤算器也是線程公有的內存區域,可以以為是線程履行字節碼的行號指導器(指向一條指令),java履行時經由過程轉變計數器的值來獲的下一條須要履行的指令,分支、輪回、跳轉、異常處置、線程恢復等履行次序都要依附這個計數器來完成。虛擬機的多線程是經由過程輪番切換並分派處置器履行時光完成,處置器(對多核處置器來講是一個內核)在一個時辰只能在履行一條敕令,是以線程履行切換後須要恢復到准確的履行地位,每一個線程都有一個自力的法式盤算器。

在履行一個java辦法時,這個法式盤算器記載(指向)以後線程正在履行的字節碼指令地址,假如正在履行的是Native辦法,這個盤算器的值為undefined,這是由於HotSpot虛擬機線程模子是原生線程模子,即每一個java線程直接映照OS(操作體系)的線程,履行Native辦法時,由OS直接履行,虛擬機的這個計數器的值是無用的;因為這個盤算器是一塊占用空間很小的內存區域,為線程公有,不須要擴大,是虛擬機標准中獨一一個沒有劃定任何OutOfMemoryError異常的區域。

4、堆內存(Heap)

java 堆是線程同享的內存區域,可以說是虛擬機治理的內存最年夜的一塊區域,在虛擬機啟動時創立;java堆內存重要是存儲對象實例,簡直一切的對象實例(包含數組)都是存儲在這裡,是以這也是渣滓收受接管(GC)最重要的內存區域,有關GC的內容這裡不做描寫;

依照虛擬機標准,java堆內存可以處於不持續的物理內存中,只需邏輯上是持續的,而且空間擴大也沒無限制,既可所以固定年夜小,也能夠是棵擴大的;假如堆內存沒有足夠的空間完成實例分派,並且也沒法擴大,將會拋出OutOfMemoryError異常。

5、辦法區

辦法區和堆內存一樣,是線程同享的內存區域;存儲曾經被虛擬機加載的類型信息、常量、靜態變量、即時編譯期編譯後的代碼等數據;虛擬機標准關於辦法區的完成沒有過量限制,和堆內存一樣不須要持續的物理內存空間,年夜小可以固定或許可擴大,還可以選擇不完成渣滓收受接管;當辦法區沒法知足內存分派需求時將會拋出OutOfMemoryError異常。

6、直接內存

直接內存其實不是虛擬機治理內存的一部門,然則這部門內存照樣能夠被頻仍用到;在java法式應用到Native辦法時(如 NIO,有關NIO這裡不做描寫),能夠會直接在堆外分派內存,然則內存總空間年夜小是無限的,也會碰到內存缺乏的情形,一樣會拋出OutOfMemoryError異常。

2、實例對象存儲拜訪

下面第一點對虛擬機各區域內存有個整體的描寫,關於每一個區域,都存在數據是若何創立、結構、拜訪的成績,我們以最常應用的的堆內存為例基於HotSpot說下這三個方面。

1、實例對象創立

當虛擬機履行到一條new指令時,起首起首從常量池定位這個創立對象的類符號援用、斷定檢討類能否曾經加載初始化,假如沒有加載,則履行類加載初始化進程(關於類加載,這裡不做描寫),假如這個類找不到,則拋出罕見的ClassNotFoundException異常;

經由過程類加載檢討後,就是現實為對象分派物理內存(堆內存),對象所需的內存空間年夜小是由對應的類肯定的,類加載後,這個類的對象所需的內存空間是固定的;為對象分派內存空間,相當於要從堆中劃分出一塊出來分派給這個對象;

依據內存空間能否持續(已分派和未分派是辨別為完全的兩部門)分為兩種分派內存方法:

1. 持續的內存:已分派和未分派中央應用一個指針作為分界點,對象內存分派只須要指針向未分派內存段挪動一段空間年夜小便可;這類方法稱 為“指針碰撞”。

2. 非持續內存:虛擬機須要保護(記載)一個列表,記載堆中那些內存塊的沒有分派的,在分派對象內存時從當選擇一塊合適年夜小的內存區域 分派給對象,並更新這個列表;這類方法稱為“余暇列表”。

對象內存的分派也會碰到並發的成績,虛擬機應用兩種計劃處理這個線程平安成績:第一應用CAS(Compare and set)+辨認重試,包管分派操作的原子性;第二是內存分派依照線程劃分分歧的空間,即每一個線程在堆中事後分派好一塊線程公有的內存,稱為當地線程分派緩存區(Thread Local Allocation Buffer,TLAB);誰人線程要分派內存時,直接從TLAB平分配出來,只要當線程的TLAB分派完須要從新分派,才須要同步操作從堆平分配,這個計劃有用的削減線程間對象分派堆內存的並發情形湧現;虛擬機能否應用TLAB這類計劃,是經由過程JVM參數 -XX:+/-UseTLAB 設定。

完成內存分派後,除對象頭信息外,虛擬機遇將分派到的內存空間初始化為零值,包管對象實例的字段可以不賦值便可直接應用到數據類型對應的零值;緊接著,履行 init 辦法依照代碼完成初始化,才完成一個實例對象的創立;

2、對象在內存的結構

在HotSpot虛擬機中,對象在內存分為3個部門:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding):

個中對象頭又分兩個部門:一部門存儲對象運轉時數據,包含哈希碼、渣滓收受接管分代年紀、對象鎖狀況、線程持有的鎖、傾向線程ID、傾向 時光戳等;在32位和64位虛擬機中,這部門數據分離占用32位和64位;因為運轉時數據較多,32位或許64位缺乏以完整存儲全體數據,所以 這部門設計為非固定格局存儲運轉時數據,而是依據對象的狀況分歧而應用分歧位來存儲數據;另外一部門存儲對象類型指針,指向這個對象的 類,但這其實不是必需的,對象的類元數據紛歧定要應用這部門存儲來肯定(上面會講到);

實例數據則是存儲對象界說的各類類型數據的內容,而這些法式界說的數據其實不是完整依照界說的次序存儲的,它們是依照虛擬機分派戰略和界說的次序肯定:long/double、int、short/char、byte/boolean、oop(Ordinary Object Ponint),可以看出,戰略是依照類型占位若干分派的,雷同的類型會在一路分派內存;並且,在知足這些條件前提下,父類變量次序先於子類;

而對象填充這部門不是必定會存在,它僅僅是起到占位對齊的感化,在HotSpot虛擬機內存治理是依照8字節為單元治理,是以當分派完內存後,對象年夜小不是8的倍數,則由對齊填充補全;

3、對象的拜訪
在java法式中,我們創立了一個對象,現實上我們獲得一個援用類型變量,經由過程這個變量來現實操作一個在堆內存中的實例;在虛擬機標准中,只劃定了援用(reference)類型是指向對象的援用,沒有劃定這個援用是若何去定位、拜訪到堆中實例的;今朝主流的虛擬機中,重要有兩種方法完成對象的拜訪:

1. 句柄方法:堆內存中劃分出一塊區域作為句柄池,援用變量中存儲的是對象的句柄地址,而句柄中存儲了示例對象和對象類型的詳細地址信息,是以對象頭中可以不包括對象的類型:



2. 指針直接拜訪:援用類型直接存儲的是實例對象在堆中的地址信息,然則這就必需請求實例對象的結構中,對象頭必需包括對象的類型:


這兩種拜訪方法各有優勢:當對象地址轉變(內存整頓、渣滓收受接管),句柄方法拜訪對象,援用變量不須要轉變,只須要轉變句柄中的對象地址值便可;而應用指針直接拜訪方法,則須要修正這個對象全體的援用;然則指針方法,可以削減一次尋址操作,在年夜量對象拜訪的情形下,這類方法的優勢比擬顯著;HotSpot虛擬機就是應用這中指針直接拜訪方法。

3、運轉時內存異常

java法式內存在運轉時重要能夠產生兩種異常情形:OutOfMemoryError、StackOverflowError;誰人內存區域會產生甚麼異常,後面曾經簡略提到,除法式計數器已外,其他內存區域都邑產生;本節重要經由過程實例代碼演示各個內存區域產生異常的情形,個中會應用到很多經常使用的虛擬機啟動參數以便更好解釋情形。(若何應用參數運轉法式這裡不做描寫)

1、java堆內存溢出

堆內存溢動身生在堆容量到達最年夜堆容量後創立對象情形下,在法式中只需赓續的創立對象,而且包管這些對象不會被渣滓收受接管:

/**
 * 虛擬機參數:
 * -Xms20m 最小堆容量
 * -Xmx20m 最年夜堆容量
 * @author hwz
 *
 */
public class HeadOutOfMemoryError {

  public static void main(String[] args) {
    //應用容器保留對象,包管對象不被渣滓收受接管
    List<HeadOutOfMemoryError> listToHoldObj = new ArrayList<HeadOutOfMemoryError>();

    while(true) {
      //赓續創立對象並參加容器中
      listToHoldObj.add(new HeadOutOfMemoryError());
    }
  }
}

這裡可以加上虛擬機參數:-XX:HeapDumpOnOutOfMemoryError,在發送OOM異常的時刻讓虛擬機轉儲以後堆的快照文件,後續可以經由過程這個文件分詞異常成績,這個不做具體描寫,後續再寫個博客具體描寫應用MAT對象剖析內存成績。

2、虛擬機棧和當地辦法棧溢出

在HotSpot虛擬機中,這兩個辦法棧是沒有一路完成的,依據虛擬機標准,這兩塊內存區域會產生這兩種異常:

1. 假如線程要求棧深度年夜於虛擬機許可的最年夜深度,拋出StackOverflowError異常;

2. 假如虛擬機在擴大棧空間時,沒法請求年夜內存空間,將拋出OutOfMemoryError異常;

這兩種情形現實上是存在堆疊的:當棧空間沒法持續分派是,究竟是內存太小照樣已應用的棧深度太年夜,這個沒法很好的辨別。

應用兩種方法測試代碼

1. 應用-Xss參數削減棧年夜小,無窮遞歸挪用一個辦法,無窮加年夜棧深度:

/**
 * 虛擬機參數:<br>
 * -Xss128k 棧容量
 * @author hwz
 *
 */
public class StackOverflowError {

  private int stackDeep = 1;

  /**
   * 無窮遞歸,無窮加年夜挪用棧深度
   */
  public void recursiveInvoke() {
    stackDeep++;
    recursiveInvoke();
  }
  public static void main(String[] args) {
    StackOverflowError soe = new StackOverflowError();

    try {
      soe.recursiveInvoke();
    } catch (Throwable e) {
      System.out.println("stack deep = " + soe.stackDeep);
      throw e;
    }
  }
}

辦法中界說年夜量當地變量,增長辦法棧中當地變量表的長度,異樣無窮遞歸挪用:

/**
 * @author hwz
 *
 */
public class StackOOMError {

  private int stackDeep = 1;

  /**
   * 界說年夜量當地變量,增年夜棧中當地變量表
   * 無窮遞歸,無窮加年夜挪用棧深度
   */
  public void recursiveInvoke() {
    Double i;
    Double i2;
    //.......此處省略年夜質變量界說
    stackDeep++;
    recursiveInvoke();
  }
  public static void main(String[] args) {
    StackOOMError soe = new StackOOMError();

    try {
      soe.recursiveInvoke();
    } catch (Throwable e) {
      System.out.println("stack deep = " + soe.stackDeep);
      throw e;
    }
  }
}

以上代碼測試解釋,不管是幀棧太年夜照樣虛擬機容量太小,當內存沒法分派時,拋出的都是StackOverflowError異常;

3、辦法區和運轉經常量池溢出

這裡先描寫一下String的intern辦法:假如字符串常量池曾經包括一個等於此String對象的字符串,則前往代表這個字符串的String對象,不然將此String對象添加到常量池中,並前往此String對象的援用;經由過程這個辦法赓續在常量池中增長String對象,招致溢出:

/**
 * 虛擬機參數:<br>
 * -XX:PermSize=10M 永遠區年夜小
 * -XX:MaxPermSize=10M 永遠區最年夜容量
 * @author hwz
 *
 */
public class RuntimeConstancePoolOOM {

  public static void main(String[] args) {

    //應用容器保留對象,包管對象不被渣滓收受接管
    List<String> list = new ArrayList<String>();

    //應用String.intern辦法,增長常量池的對象
    for (int i=1; true; i++) {
      list.add(String.valueOf(i).intern());
    }
  }
}

然則這段測試代碼在JDK1.7下沒有產生運轉經常量池溢出,在JDK1.6卻是會產生,為此再寫一段測試代碼驗證這個成績:

/**
 * String.intern辦法在分歧JDK下測試
 * @author hwz
 *
 */
public class StringInternTest {

  public static void main(String[] args) {

    String str1 = new StringBuilder("test").append("01").toString();
    System.out.println(str1.intern() == str1);

    String str2 = new StringBuilder("test").append("02").toString();
    System.out.println(str2.intern() == str2);
  }
}

在JDK1.6下運轉成果為:false、false;

在JDK1.7下運轉成果為:true、true;

本來在JDK1.6中,intern()辦法把初次碰到的字符串實例復制到永遠代,反回的是永遠代中的實例的援用,而有StringBuilder創立的字符串實例在堆中,所以不相等;

而在JDK1.7中,intern()辦法不會復制實例,只是在常量池記載初次湧現的實例的援用,是以intern前往的援用和StringBuilder創立的實例是統一個,所以前往true;

所以常量池溢出的測試代碼不會產生常量池溢出異常,而是在赓續運轉後能夠產生堆內存缺乏溢出異常;

那要測試辦法區溢出,只需赓續往辦法區參加器械就好了,好比類名、拜訪潤飾符、常量池等。我們可讓法式加載年夜量的類去赓續填充辦法區從而招致溢出,這個我們應用CGLib直接操作字節碼生成年夜量靜態類:

/**
 * 辦法區內存溢出測試類
 * @author hwz
 *
 */
public class MethodAreaOOM {

  public static void main(String[] args) {
    //應用GCLib無窮靜態創立子類
    while (true) {
      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(MAOOMClass.class);
      enhancer.setUseCache(false);
      enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object obj, Method method, Object[] args,
            MethodProxy proxy) throws Throwable {
          return proxy.invokeSuper(obj, args);
        }
      });
      enhancer.create();
    }
  }

  static class MAOOMClass {}
}

經由過程VisualVM不雅察可以看到,JVM加載類的數目和PerGen的應用成直線上升:

4、直接內存溢出

直接內存的年夜小可以經由過程虛擬機參數設定:-XX:MaxDirectMemorySize,要使直接內存溢出,只須要赓續的請求直接內存便可,以下同Java NIO 中直接內存緩存測試:

/**
 * 虛擬機參數:<br>
 * -XX:MaxDirectMemorySize=30M 直接內存年夜小
 * @author hwz
 *
 */
public class DirectMemoryOOm {

  public static void main(String[] args) {
    List<Buffer> buffers = new ArrayList<Buffer>();
    int i = 0;
    while (true) {
      //打印以後第幾回
      System.out.println(++i);
      //經由過程赓續請求直接緩存區內存消費直接內存
      buffers.add(ByteBuffer.allocateDirect(1024*1024)); //每次請求1M
    }
  }
}

在輪回中,每次請求1M直接內存,設置最年夜直接內存為30M,法式運轉到31次時拋出異常:java.lang.OutOfMemoryError: Direct buffer memory

4、總結

以上就是本文的全體內容,本文重要描寫JVM中內存的結構構造、對象存儲和拜訪曾經各個內存區域能夠湧現的內存異常;重要參考書目《深刻懂得Java虛擬機(第二版)》,若有不准確的地方,還請在評論中指出;感謝年夜家對的支撐。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved