(1)Java虛擬機:Java內存區域與內存溢出異常

Java虛擬機內存劃分
這裏寫圖片描述

  1. 程序計數器:
    (1)線程私有。
    (2)記錄當前線程所執行的程序碼位置。因爲一個程序可能會出現多個線程,而多個線程執行時又是交替執行的,所以就需要記錄各個線程的執行位置,以便之後繼續執行。
    (3)如果一個線程正在執行Java方法,則其計數的值爲正在執行的虛擬機字節碼指令的地址;如果是Native方法(調用C,C++等其他語言),則計數爲 undefined。
    (4)唯一一個沒有規定任何OutOfMemoryError異常情況的內存區域。

  2. Java虛擬機棧:
    (1)線程私有,生命週期與線程相同。
    (2)當每個方法運行時,都會創建一個棧幀(Stack Frame)用來保存局部變量表,操作數棧,動態連接,方法出口(方法返回地址)等信息。而每個方法從被調用到執行完成的過程,就是一個棧幀在虛擬機棧中從入棧到出棧的過程。

    1. 局部變量表:
      存儲着編譯器可知的基本數據類型(boolean,byte,char,short,int,float,long,double),對象引用(reference類型)和returnAddress類型(指向一條字節碼指令的地址)。64位的long和double會佔用2個局部變量空間(Slot),其他1個。
      局部變量表所需的內存空間在編譯期完成分配,即進入一個方法時,該方法在幀中所要分配的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

    2. 操作棧:
      棧深度在編譯期確定,在棧幀生成時置爲空。在執行方法操作時,存儲JVM從局部變量表中複製的常量或變量,提供提取及結果入棧。也用於存放執行方法所需的參數和方法運行的結果。
      可以存放JVM中定義的任意一種類型的變量。
      任意時刻,操作棧都是固定深度的,long,double佔用2個深度,其餘佔1個。

    3. 動態連接:
      每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。Class文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用,一部分會在類加載階段或第一次使用的時候轉化爲直接引用(如final、static域等),稱爲靜態解析,另一部分將在每一次的運行期間轉化爲直接引用,這部分稱爲動態連接。

    4. 方法返回地址:
      顧名思義,就是當前棧幀所代表的方法被調用的地址。
      一個方法被執行後有2種退出方式:1.遇到方法返回的字節碼指令。2.遇到異常並沒有在方法中進行處理。
      方法退出實際上就是該方法對應的棧幀在Java虛擬機棧中出棧的過程。一般來說執行的操作有:回覆調用者的局部變量表,操作數棧。如果有返回值,則將返回值放入調用者棧幀的操作數棧中。程序計數器移動至調用處指令的後一條指令

    該區域規定了兩種異常
    1.當線程請求的棧深度超過了虛擬機所允許的深度,則會拋出StackOverFlowError;
    2.如果虛擬機棧可以動態擴展(現在大部分都是可以的)則如果在擴展時無法申請到足夠的內存會拋出OutOfMemoryError。

    這裏結合下《java虛擬機規範中文版》第二章第五節中的描述:
    1.如果線程請求分配的棧容量超過java虛擬機棧允許的最大容量的時候,java虛擬機將拋出一個StackOverFlowError異常。
    2.如果java虛擬機棧可以動態拓展,並且擴展的動作已經嘗試過,但是目前無法申請到足夠的內存去完成拓展,或者在建立新線程的時候沒有足夠的內存去創建對應的虛擬機棧,那java虛擬機將會拋出一個OutOfMemoryError異常。

    個人理解:一般在使用無限深度的遞歸時會拋出1,原因是因爲程序不停的在方法中調用新的方法,所以達到一個量級後就會超過Java虛擬機棧所能容納棧幀的最大深度,但是這裏有一個問題,就是在無限深度的遞歸時也可能出現Java虛擬機棧內存的耗盡(因爲每個方法的調用都會產生棧幀,也會往局部變量表中存東西,這些都要內存分配),那麼這裏是如何區分棧深度耗盡和棧內存耗盡的呢?這裏結合了下查閱的資料。

    首先看下下面的代碼:
    單線程的情況下,下面的代碼都是拋出StackOverFlowError異常,也就是說在單線程的情況下,無論是由於Java虛擬機棧深度不足或者Java虛擬機棧內存不足,都會拋出StackOverFlowError異常。

public class MyJVMTest {
    public static void main(String [] args){
        MyJVMTest myJVMTest=new MyJVMTest();
        myJVMTest.StackTest();
    }
    public void StackTest(){
        StackTest();
    }
}

這裏寫圖片描述

多線程情況:
因爲運行後公司電腦CPU瞬間爆炸(彷彿聞到了CPU的香味_ (:3」∠*)_),建議慎重運行。跑了4-5分鐘,還是沒有耗盡內存,查看相關的資料,理論上是可以拋出如下異常的:
Exception in thread”main”java.lang.OutOfMemoryError:unable to create new native thread
綜合了下資料後,簡單地說:
a)StackOverflowError(方法調用層次太深,內存不夠新建棧幀)
b)OutOfMemoryError(線程太多,內存不夠新建線程)

    public static void main(String [] args){
        MyJVMTest myJVMTest=new MyJVMTest();
        myJVMTest.StackOutOfMemoryError();
    }
    //JVM Stack OOME
    public void StackOutOfMemoryError(){
        for(int i=0;;i++){
            System.out.println(i);
            Thread thread=new Thread(new Runnable(){
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    while(true){
                    }
                }
            });
            thread.start();
        }
    }

這裏寫圖片描述


3. 本地方法棧:
(1)與Java虛擬機棧沒有實際區別,只不過Java虛擬機棧是虛擬機用來執行Java方法(字節碼)的,而本地方法棧是用來執行Native方法的,所以不同的虛擬機實現對這一塊可以有不同的實現。
(2)與Java虛擬機棧一樣,規定了StackOverFlowError和OutOfMemoryError異常。

4. Java堆:
(1)Java堆被所有線程共享。在虛擬機啓動時創建。
(2)Java堆的唯一目的就是用來存放對象實例。幾乎所有的對象實例都存儲在Java堆中,但是因爲現在新技術的出現,所以這點也不是絕對的了。
(3)是垃圾回收器管理的主要區域。
(4)可以是物理上不連續的內存空間,只要邏輯上連續即可。並且可以是固定大小的,也可以是可擴展的(現在大部分主流實現都是可擴展的)
(5)如果當堆中沒有內存完成實例分配,並且也無法再被擴展時,會拋出OutOfMemoryError異常。
順便,正好講下遇到的其他幾種OOME

//1
    public static void main(String [] args){
        MyJVMTest myJVMTest=new MyJVMTest();
        myJVMTest.MemoryTest();
    }
    //GC OOME
    public void MemoryTest(){
        Map<String,Object> memory=new HashMap<String,Object>();
        for(int i=0;;i++){
            double x=i;
            System.out.println(x);
            memory.put(Integer.toString(i), x);
        }
    }

這裏寫圖片描述

//2
public class MyJVMTest {
    public static void main(String [] args){
        MyJVMTest myJVMTest=new MyJVMTest();
        myJVMTest.MemoryTest();
    }
    public void MemoryTest(){
        Map<String,Object> memory=new HashMap<String,Object>();
        for(int i=0;;i++){
            int[] testStr=new int[2000000];
            memory.put(Integer.toString(i), testStr);
        }
    }
}

這裏寫圖片描述

上面2個其實都是Java堆拋出的,第一個所報的GC overhead limit exceeded異常,可以看做是垃圾回收器發出的警報,具體出現原因如下:
GC overhead limt exceed檢查是Hotspot VM 1.6定義的一個策略,通過統計GC時間來預測是否要OOM了,提前拋出異常,防止OOM發生。Sun 官方對此的定義是:“並行/併發回收器在GC回收時間過長時會拋出OutOfMemroyError。過長的定義是,超過98%的時間用來做GC並且回收了不到2%的堆內存。用來避免內存過小造成應用不能正常工作。“
對於上面的異常代碼1,還可以引申出一些東西,就是Java的自動裝箱機制和Java中所有的基本類型是否都是存在Java虛擬機棧中的?
第一點就是double類型在被put入Map中時,因爲自動裝箱機制的存在,所以會被包裝爲Double類型.
第二點比較容易被誤導,實際上在方法中定義的基本變量是存放在Java虛擬機棧中的,比如方法中int i=0;其中變量i和0都是存在Java虛擬機棧中的局部變量表裏的。而類中的基本類型成員,是全局變量,在實例化爲對象時,是和對象實例一起存在Java堆中的。
所以在上面的代碼1中,是由Java堆拋出的異常。而不是Java虛擬機棧拋出的。
還有一點就是上面的代碼2中,如果將2000000改爲20,則也會出現代碼1的異常,這裏對照Sun官方定義,也能很清楚的理解GC OOME出現的原因。

5. 方法區:
(1)方法區(Method Area)與Java堆一樣,都是所有線程共享的內存區域。用來存放類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。Java虛擬機規範描述中方法區爲Java堆的邏輯部分,但是方法區有一個別名Non-Heap(非堆),目的應該是與Java堆區分開來。
(2)方法區與Java堆一樣,可以是物理上不連續的內存空間,大小也可以是固定的或者可擴展的。另外,還可以選擇不實現垃圾回收器,相對而言該區域垃圾回收行爲在該區域是比較少出現的,但也不是說存儲在這裏的數據就是永久的,通常這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。而且對該區域的垃圾回收效果一般比較難以讓人滿意。尤其是對類的卸載,條件極其苛刻。
(3)根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

6. 運行時常量池:
(1)方法區的一部分,Class文件中除了有版本,字段,方法,接口等描述信息外,還有一項是運行時常量池,用來存放編譯時產生的各種字面量和符號引用,這些將在類加載後存放到方法區的運行時常量池中。
(2)Java虛擬機對Class文件中的每一部分都有嚴格的規定,但是對於運行時常量區來說,沒有說明細節要求。不過一般來說除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
(3)運行時常量池相對於Class文件中常量池的另外一個重要特徵是具備動態性,即並非只有被預置入Class文件常量池中的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池內。
(4)既然運行時常量池是方法區的一部分,自然會受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章