1. 程式人生 > >學習jvm(一)--java內存區域

學習jvm(一)--java內存區域

express ria java開發 進行 自定義對象 java語言 生命 指向 文件中

前言

通過學習深入理解java虛擬機的教程,以及自己在網上的查詢的資料,做一個對jvm學習過程中的小總結。

本文章內容首先講解java的內存分布區域,之後講內存的分配原則以及內存的監控工具。再下來會著重講解垃圾回收這一章節,該章節涉及了垃圾的標記算法以及各種垃圾回收算法,然後大概的介紹下市面上使用的垃圾收集器。之後就總結下上面的原理,講解相關的jvm調優案例。然後會著重講解類加載過程。最後一章講字節碼的部分,字節碼相對來說是比較枯燥而且特別繁瑣的內容,最好是自己動手配合著學習會好一點,或者觀其大略,不求甚解也可。

java內存區域

? 先來一段簡單的java創建對象的代碼熱一下身,如下,

//運行時, jvm把TestObjectCreate的信息都放入方法區 
public class TestObjectCreate {
    //main方法本身放入方法區
    public static void main(String[] args) {
        Sample test1 = new Sample("測試1");
        //test1是引用,所以放到棧區裏,Sample是自定義對象應該放到堆裏面
        Sample test2 = new Sample("測試2");

        test1.printName();
        test2.printName();
    }
}

//運行時, jvm把Sample的信息都放入方法區 
class Sample {
    //new Sample實例後,name引用放入棧區裏,name對象放入堆裏
    private String name;

    public Sample(String name) {
        this.name = name;
    }

    public void printName() {
        System.out.println(name);
    }
}

? 棧、堆、方法區的交互圖如下,

技術分享圖片

? 進入正題,java的內存區域一般分成兩大區域:線程獨占區和線程共享區。

? 線程獨占區:虛擬機棧、本地方法棧、程序計數器

? 線程共享區:堆、方法區

技術分享圖片

線程獨占區

? 顧名思義,線程獨占區就是這塊內存區域在運行過程中,是每個線程獨有的內存空間。在這塊內存空間中,就有我們平常所認知的,專業的術語叫虛擬機棧,因為還有另外一個區域叫本地方法棧,然後除了前面說的這兩個棧外,還有一個程序計數器。線程獨占區大概分為這三塊內容。

虛擬機棧

? 虛擬機棧就是我們日常開發中經常提到的棧了,這裏其實有幾個概念:棧幀局部變量表操作數棧動態連接返回地址

技術分享圖片

棧幀

? 棧幀(stack frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧的棧元素。

? 我們平常所說的方法進棧出棧,就是指棧幀。每一個方法從調用開始到執行完成的過程,就對應著一個棧幀在虛擬機棧裏面從入棧到出棧的過程。 對於執行引擎來說,活動線程中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法。執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。

? 在Java程序被編譯成class文件時,棧幀的大小就已經確定,在jvm運行過程時棧幀的大小是不會改變的

? 棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息。

局部變量表

? 局部變量表是一塊比較 重點的內容,因為他在平常的java開發的過程中,接觸的還是比較直接的,所以對於局部變量表的內容需要記住與理解。局部變量表不止包含我們方法中的局部變量,還包含了參數列表也會保存進局部變量表中,而且非static方法的this指針也會store進局部變量表。

? 局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。

? 我們平常所說的new了一個對象,然後在堆中開辟了一段空間,然後在棧中添加了一個引用指向了堆的這段空間,而這裏添加的一個引用就是在局部變量表。

? 系統不會為局部變量賦予初始值(實例變量和類變量都會被賦予初始值)。也就是說不存在類變量那樣的準備階段

? 在Java程序被編譯成class文件時,就在方法的Code屬性的max_locals數據項中確定了該方法所需要分配的最大局部變量表的容量。

? 局部變量表的容量以變量槽(Slot)為最小單位,32位虛擬機中一個Slot可以存放一個32位以內的數據類型(boolean、byte、char、short、int、float、reference和returnAddress八種)。

? reference類型虛擬機規範沒有明確說明它的長度,但一般來說,虛擬機實現至少都應當能從此引用中直接或者間接地查找到對象在Java堆中的起始地址索引和方法區中的對象類型數據。

? returnAddress類型是為字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。

? 對於64位的數據類型,虛擬機會以高位在前的方式為其分配兩個連續的Slot空間。Java語言中明確規定的64位的數據類型只有long和double數據類型分割存儲的做法與"long和double的非原子性協定" 中把一次long 和double 數據類型讀寫分割為兩次32位讀寫的做法類似,在閱讀JAVA內存模型時對比下。不過,由於局部變量表建在線程的堆棧上,是線程私有的數據,無論讀寫兩個連續的Slot是否是原子操作,都不會引起數據安全問題。

? 虛擬機通過索引定位的方式使用局部變量表,索引值的範圍是從0開始到局部變量表最大的Slot數量。如果32位數據類型的變量,索引N就代表了使用第N個Slot,如果是64位數據類型(long、double)的變量,則說明要使用第N個和N+1兩個Slot。

? 虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,如果是實例方法(非static),那麽局部變量表的第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,在方法中通過this訪問。

? 為了盡可能節省棧幀空間,局部變量表中的Slot是可以重用的,方法體中定義的變量,其作用域並不一定會覆蓋整個方法體,如果當前字節碼PC計數器的值已經超出了某個變量的作用域,那這個變量對應的Slot就可以交給其他變量使用。

? 就是說,局部變量表Slot槽位的是可以復用的,當一個變量已經超過其作用域,即後續代碼已經不會再對其使用了,那麽jvm會復用該變量的Slot槽位給後續代碼的變量使用。那麽這裏就會帶來另外一個問題,當變量超出了其作用範圍,jvm還保留著其Slot槽位,在Slot槽位還沒被復用的時候該變量實際上就還沒被回收,如下代碼

public class TestStackLocalVariableTable {
    private static int _M = 1024 * 1024;
    public static void main(String[] args) throws InterruptedException {
        byte[] array = new byte[60 * _M];
        System.gc();
    }
}

? 代碼其實也很簡單,就是new了一個60M的字節數組,然後就調用gc垃圾回收,運行結果如下所示,在Full GC中最終也沒有去回收這個變量。這裏沒有回收array所占的內存能說得過去,因為在執行System.gc()時,變量array還處在作用域之內,虛擬機自然不敢回收這部分的內存。 那我們把代碼修改一下 。

技術分享圖片

? (ps1:控制臺中的結果是怎麽打印出來的呢?其實是在運行的時候在虛擬機上加入參數:-verbose:gc ,如下所示)

技術分享圖片

? (ps2:控制臺中的gc信息要怎麽看?如 [GC (System.gc()) 64102K->62224K(125952K), 0.0170834 secs]這個結果,表示系統進行了gc操作,然後操作的結果是從原來堆內存的使用量64102K,gc之後變成了62224K,只回收了一點點,即其實實際上回收的是其他在運行過程的其他空間,而我們定義60M的字節數組並沒有回收到,而上面的結果只是jvm進行Minor GC而已,Minor GC是只回收堆內存的Eden區域內存,後面講堆內存的時候會詳細講,這裏可以認為Minor GC是輕量級的GC回收,速度快,而在Minor GC沒有回收成功之後,jvm進行了Full GC,Full GC是掃描整個堆內存的gcroot去回收垃圾,後面也會細講,這裏可以認為Full GC是重量級的GC回收,速度慢,回收的時長可能是Minor GC的幾倍或10倍不止,返回正題,就是從運行結果[Full GC (System.gc()) 62224K->62107K(125952K), 0.0230050 secs]中看,發生了Full GC之後,也沒有回收60M字節數組這部分內存)

? 我們修改下上面的這段代碼,如下所示,其實就是將byte[] array = new byte[60 * _M]用代碼塊包起來而已

public class TestStackLocalVariableTable {
    private static int _M = 1024 * 1024;
    public static void main(String[] args) throws InterruptedException {
        {
            byte[] array = new byte[60 * _M];
        }
        System.gc();
    }
}

? 按照原來的理解,在超出代碼塊的之後,array對象已經不會再使用了,應該調用gc之後回收了才對,但是從下面的結果中可以發現,array對象還是沒有被回收,因為array的Slot槽位會被復用,jvm還保留著array在堆中的引用,那麽gc就不會回收這一部分的內存。

技術分享圖片

? 再將上面的代碼再修改下,如下,

public class TestStackLocalVariableTable {
    private static int _M = 1024 * 1024;
    public static void main(String[] args) throws InterruptedException {
        {
            byte[] array = new byte[60 * _M];
        }
        int b = 0;
        System.gc();
    }
}

? 實際上就是在第二段代碼的基礎上,加了 int b = 0而已,但是從結果可知,array對象被回收了,因為這個時候,操作了局部變量表,局部變量b復用了array的Slot槽位,Full GC時發現array對象已經沒有了引用,就把array回收了。

技術分享圖片

? 雖然現在知道了原理,但是實際上問題還沒解決,在實際開發中,朝生夕死的對象的內存肯定是要回收的,也不可能在業務代碼寫完之後,去寫一個無用的變量去操作局部變量表,那該怎麽辦呢?

? 所以,當處理完業務流程之後,將需要回收的對象,最好手動賦值為null,這樣有助於gc的回收。

public class TestStackLocalVariableTable {
    private static int _M = 1024 * 1024;
    public static void main(String[] args) throws InterruptedException {
//        int a = 0;
        {
            byte[] array = new byte[60 * _M];
            array = null; //手動賦值對象為null,array對象沒有了引用,GC會將這個對象回收
        }
//        a = 1; //讀取了局部變量表,但是沒有復用array的Slot槽位,jvm還保留著array的引用,此時GC不會回收array對象
//        int b = 0; //操作了局部變量表,復用了array的Slot槽位,array對象沒有了引用,GC會將array對象回收
        System.gc();
    }
}
操作數棧

? 每一個獨立的棧幀中除了包含局部變量表以外,還包含一個後進先出(Last-In-First-Out)的操作數棧,也可以稱之為表達式棧(Expression Stack)。操作數棧和局部變量表在訪問方式上存在著較大差異,操作數棧並非采用訪問索引的方式來進行數據訪問的,而是通過標準的入棧和出棧操作來完成一次數據訪問。每一個操作數棧都會擁有一個明確的棧深度用於存儲數值,一個32bit的數值可以用一個單位的棧深度來存儲,而2個單位的棧深度則可以保存一個64bit的數值,當然操作數棧所需的容量大小在編譯期就可以被完全確定下來,並保存在方法的Code屬性中。

? HotSpot中任何的操作都需要經過入棧和出棧來完成,那麽由此可見,HotSpot的執行引擎架構必然就是基於棧式架構,而非傳統的寄存器架構。簡單來說,操作數棧就是JVM執行引擎的一個工作區,當一個方法被調用的時候,一個新的棧幀也會隨之被創建出來,但這個時候棧幀中的操作數棧卻是空的,只有方法在執行的過程中,才會有各種各樣的字節碼指令往操作數棧中執行入棧和出棧操作。比如在一個方法內部需要執行一個簡單的加法運算時,首先需要從操作數棧中將需要執行運算的兩個數值出棧,待運算執行完成後,再將運算結果入棧。

? 下面用一個簡單的例子來說明,

public class TestOperandStack {

    public static void main(String[] args) {
        add(1, 2);
        int d = 2 + 2;
    }

    public static long add(int a, int b) {
        long c = a + b;
        return c;
    }
}

? 上面這段是java源碼,下面這段代碼是根據 javap -verbose指令打印的結果(javap是JDK自帶的反匯編器,可以查看java編譯器為我們生成的字節碼。通過它,我們可以對照源代碼和字節碼,從而了解很多編譯器內部的工作。在字節碼一章會比較詳細的解釋,沒有前置知識的可以先看字節碼的章節)

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_1
         1: iconst_2
         2: invokestatic  #2                  // Method add:(II)J
         5: pop2
         6: iconst_4
         7: istore_1
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 6
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            8       1     1     d   I

  public static long add(int, int);
    descriptor: (II)J
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: i2l
         4: lstore_2
         5: lload_2
         6: lreturn
      LineNumberTable:
        line 11: 0
        line 12: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0     a   I
            0       7     1     b   I
            5       2     2     c   J
}

? 從上往下讀javap的結果,從main的Code開始,iconst_1表示int類型常量1進操作數棧,iconst_2表示int類型常量2進操作數棧,然後invokestatic調用靜態方法即調用add方法,main暫時停下,看add的Code方法,iload_0表示從局部變量表中取出int變量進操作數棧,尾數0表示第一個Slot槽位此時即為a,然後iload_1又是一個從局部變量表中取出int變量進操作數棧即為b,然後iadd指令表示將操作數棧頂的兩個數取出並相加,即將a,b兩數從操作數棧頂取出,進行相加,i2l表示是類型轉換指令,將int類型轉換為long類型(int to long)即表示long c = a + b;這句代碼將a與b相加的結果int類型轉換為long類型,然後lstore_2表示將long類型變量即c加到局部變量表,然後lload_2從局部變量表中取出long類型的變量即為c,lreturn表示返回long類型指令。這就是上面樣例代碼的操作數棧的大致流程。

? 這裏補充一個知識點,返回main方法暫停讀下去的地方,第5行,pop2表示將操作數棧頂的2個元素出棧,即源碼中1跟2,接下的指令是iconst_4,對應源碼我們知道,此時的代碼是走到了int d = 2 + 2;,按照前面的解讀,我們理解應該是兩個iconst_2進棧,然後做iadd操作,但是此處的字節碼指令確是iconst_4,說明是編譯器做了優化,也就是編譯器優化,將在編譯期間可以計算的結果先計算出來,然後在運行期間直接取值即可,少了一步計算的操作,因為java的原則是一次編譯,多次運行,所以編譯期間能計算的結果,就在編譯器處理了

? 其實使用javap查看字節碼信息還是一件挺有意思的事情,建議還沒玩過的同學可以自己寫些代碼調試調試,看看結果,你可以發現以前看不見摸不著的原理性的東西,調試之後會有不一樣的理解,像“==”,equals,++i,i++等等。

動態連接

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

返回地址

? 當一個方法執行完畢之後,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址,以恢復上層方法的局部變量表和操作數棧,把返回值(如果有的話)壓入調用者棧幀的操作數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令

本地方法棧

? 本地方法,即為native方法,在看jdk源碼的時候,可以經常看到,像Object的getClass方法、hashCode方法等都是native方法,這些方法不是用Java實現的,本地方法本質上是依賴於實現的,虛擬機實現的設計者們可以自由地決定使用怎樣的機制來讓Java程序調用本地方法。

? 當某個線程調用一個本地方法時,它就進入了一個全新的並且不再受虛擬機限制的世界。本地方法可以通過本地方法接口來訪問虛擬機的運行時數據區,但不止如此,它還可以做任何它想做的事情。

? 任何本地方法接口都會使用某種本地方法棧。當線程調用Java方法時,虛擬機會創建一個新的棧幀並壓入Java棧。然而當它調用的是本地方法時,虛擬機會保持Java棧不變,不再在線程的Java棧中壓入新的幀,虛擬機只是簡單地動態連接並直接調用指定的本地方法。

  如果某個虛擬機實現的本地方法接口是使用C連接模型的話,那麽它的本地方法棧就是C棧。當C程序調用一個C函數時,其棧操作都是確定的。傳遞給該函數的參數以某個確定的順序壓入棧,它的返回值也以確定的方式傳回調用者。同樣,這就是虛擬機實現中本地方法棧的行為。

? 很可能本地方法接口需要回調Java虛擬機中的Java方法,在這種情況下,該線程會保存本地方法棧的狀態並進入到另一個Java棧。

  下圖描繪了這樣一個情景,就是當一個線程調用一個本地方法時,本地方法又回調虛擬機中的另一個Java方法。這幅圖展示了Java虛擬機內部線程運行的全景圖。一個線程可能在整個生命周期中都執行Java方法,操作它的Java棧;或者它可能毫無障礙地在Java棧和本地方法棧之間跳轉。

技術分享圖片

? 該線程首先調用了兩個Java方法,而第二個Java方法又調用了一個本地方法,這樣導致虛擬機使用了一個本地方法棧。假設這是一個C語言棧,其間有兩個C函數,第一個C函數被第二個Java方法當做本地方法調用,而這個C函數又調用了第二個C函數。之後第二個C函數又通過本地方法接口回調了一個Java方法(第三個Java方法),最終這個Java方法又調用了一個Java方法(它成為圖中的當前方法)。

? 本地方法棧與虛擬機棧其實很類似,只是虛擬機棧為虛擬機執行java方法服務,而本地方法棧為虛擬機執行本地方法服務。

程序計數器

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

? 由於java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令。因此,為了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域為“線程獨占區”的內存(其實這點也很好理解,每個線程在執行字節碼指令的時候,肯定是讀取各自的指令的行號位置去執行,如果是線程共享,那指令不就全亂套了嗎)

? 如果線程正在執行一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器為空(undefined)

? 此內存區域是唯一一個在java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。 (因為這部分空間是完全jvm自己管理的,與程序員無關,而且所占內存也很小)

1. 通俗解釋:

? 對於一個運行中的java程序而言,其中的每一個線程都有他自己的PC(程序計數器)寄存器,他是在該線程啟動時創建的,PC寄存器的大小是一個字長,因此它既能夠持有一個本地指針,也能持有一個returnAddress(returnAddress類型會被java虛擬機的jsr、ret和jsr_w指令所使用。returnAddress類型的值只想一條虛擬機指令的操作碼。與前面介紹的那些數值類的原生類型不同,returnAddress類型在java語言之中並不存在相應的類型,也無法在程序運行期間更改returnAddress類型的值。)。當線程執行某個java方法時 ,PC寄存器的內容總是下一條被執行指令的”地址”,這裏的”地址”可以是一個本地指針,也可以是在方法字節碼中相對於該方法起始指令的偏移量。如果該線程正在執行一個本地方法,那麽PC寄存器的值是”Undefined”。

2.本地方法和java方法:

? java中有兩種方法:java方法和本地方法。java方法是有java語言編寫,編譯成字節碼,存儲在class文件中的。本地方法是有其它語言(比如C,C++,或者是會變語言)編寫的,編譯成和處理器相關的機器代碼。本地方法保存在動態連接庫中,格式是各個平臺專用的。java方法是與平臺無關的,但是在本地方法卻不是。運行中的java程序調用本地方法時,虛擬機裝載包含這個本地方法的動態庫,並調用這個方法。

線程共享區

? 線程共享區即在運行過程中,是每個線程共享的內存空間。其中包含方法區

? java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。這一點在java虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配,但是隨著jit編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那麽“絕對”了。

? java堆可以是物理上不連續的空間,只要邏輯上連續即可,主流的虛擬機都是按照可擴展的方式來實現的。如果當前對中沒有內存完成對象實例的創建,並且不能在進行內存擴展,則會拋出OutOfMemory異常。

? Java堆是垃圾收集器管理的主要區域,所以也稱為“GC堆”。 如果從內存回收的角度看,由於現在收集器基本都是采用的分代收集算法,所以java堆中還可以細分為:新生代和老年代;再細致一點的有Eden空間、From Survivor空間、To Survivor空間等。

技術分享圖片

? 在分代收集算法中,堆的設計如上圖所示,分成新生代與老年代,而在新生代中再細分出一個大概占80%空間的Eden區域,與兩塊各占百分10%空間的Survivor區域(空間比例可以配置),為什麽這麽分,在這章就不說了,因為內容太多,而且涉及到了垃圾回收的知識,所以這塊內容就放到垃圾回收的章節去講。

? 寫段代碼,驗證下堆內存的分代,如下,

public class TestHeap {
    private static int _M = 1024 * 1024;
    @Test
    public void testParallel() {
        byte[] array = new byte[2*_M];
    }
}

? 代碼也很簡單,只是創建一個2M大小的數組而已,但是要跟蹤堆內存跟gc的話,還需要在運行時加入如下幾項jvm參數

-verbose:gc //設置跟蹤gc
-XX:+PrintGCDetails //打印詳細的gc內容
-Xms20M //設置堆內存最小內存為20M
-Xmx20M //設置堆內存最大內存為20M
-Xmn10M //設置新生代內存為10M

? 運行結果如下,可以看出我本機的jvm默認的垃圾收集器(parallel)采用的分代收集算法,堆內存進行了分代管理

技術分享圖片

技術分享圖片

? 從上面的結果圖中,不知道有沒有人發現一個問題,就是我明明設置了新生代是10M,老年代是10M(堆內存20M-新生代10M),但是上面的結果,新生代可用空間是9216K,老年代可用空間是10240K,老年代是10M沒錯,但是新生代怎麽是9M,少了1M?這個問題跟復制算法有關,在復制算法中,有一塊內存區域是用來做復制遷移對象用的,不算入實際可用的空間。

? 另外,最低邊界與最高邊界,指的這片內存空間的起始位置與終止位置,如新生代的邊界:(0x0000000100000000-0x00000000ff600000)/1024/1024=10M,然後,當前邊界是什麽呢?就是當前可用空間的內存邊界,這樣幹說的話不好理解,再做個實驗看下結果就能明白了。

? 調整下jvm的參數,將上面的 -Xmx20M,改為-Xmx40M即可,其他參數不變,再重新運行上面的程序,結果如下

技術分享圖片

? 剛剛的程序,我們設置的jvm的最小堆大小為20M,而最大堆大小為40M,但是我們發現,新生代的空間已經固定為10M,而老年代的可用空間total實際上還是10M,這是因為jvm在內存夠用的情況下,會去維持一個最小的堆空間,所以,此時程序所占空間就eden區域用了6M多一點,遠不超過20M,所以此時可用空間就還是維持在20M,我們可以拿老年代的當前邊界去減最低邊界,去計算出來,(0x00000000fe200000-0x00000000fd800000)/1024/1024=10M,而用最高邊界減最低邊界,(0x00000000ff600000-0x00000000fd800000)/1024/1024=30M,即,堆空間確實設置了最大內存為40M,但是此時維護在最小堆內存20M。

? 另外再講幾個跟堆相關的配置參數,如下

-XX:+printGC //打印GC的簡要信息
-XX:+PrintGCTimeStamps //打印CG發生的時間戳
-Xloggc:log/gc.log //指定GC log的位置,以文件輸出,可以幫助開發人員分析問題
-XX:+PrintHeapAtGC //每次一次GC後,都打印堆信息
-Xmn //設置新生代大小
-XX:NewRatio //新生代(eden+2*s)和老年代(不包含永久區)的比值,如-XX:NewRatio=4表示新生代:老年代=1:4,即年輕代占堆的1/5
-XX:SurvivorRatio //設置兩個Survivor區和eden的比,如-XX:SurvivorRatio=8表示兩個Survivor:eden=2:8,即一個Survivor占年輕代的1/10
-XX:+HeapDumpOnOutOfMemoryError //OOM時導出堆到文件
-XX:+HeapDumpPath //導出OOM的路徑
-XX:OnOutOfMemoryError //在OOM時,執行一個腳本,如"-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p"

方法區

? 在java虛擬機規範中,將方法區作為堆的一個邏輯部分來對待,但事實上,方法區並不是堆(Non-Heap);另外,不少人的博客中,將java GC的分代收集機制分為3個代:青年代,老年代,永久代,這些作者將方法區定義為“永久代”,這是因為,對於之前的HotSpot Java虛擬機的實現方式中,將分代收集的思想擴展到了方法區,並將方法區設計成了永久代。不過,除HotSpot之外的多數虛擬機,並不將方法區當做永久代,HotSpot本身,也計劃取消永久代。

? 方法區是各個線程共享的區域,用於存儲已經被虛擬機加載的類信息(即加載類時需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。

  方法區在物理上也不需要是連續的,可以選擇固定大小或可擴展大小,並且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上執行的垃圾收集是很少的,這也是方法區被稱為永久代的原因之一(HotSpot),但這也不代表著在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的內存回收和對已加載類的卸載。

  在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以後進一步深入研究時使用。

  在方法區上定義了OutOfMemoryError:PermGen space異常,在內存不足時拋出。

  運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址,將在類鏈接階段完成翻譯);運行時常量池除了存儲編譯期常量外,也可以存儲在運行時間產生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果調用的字符“abc”已經在常量池中,則返回池中的字符串地址,否則,新建一個常量加入池中,並返回地址)。

直接內存

? 直接內存並不是虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域。在jdk1.4中加入了NIO類,引入了一種基於通道(Channel)於緩沖區(Buffer)的I/O方式,他可以使用native函數庫直接分配堆外內存,然後通過一個存儲在java堆裏面的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在java堆中和native堆中來回復制數據。

? 顯然,本機直接內存的分配不會受到Java 堆大小的限制,但是,既然是內存,則肯定還是會受到本機總內存(包括ram及swap區或者分頁文件)的大小及處理器尋址空間的限制。服務器管理員配置虛擬機參數時,一般會根據實際內存設置-Xmx等參數信息,但經常會忽略掉直接內存,使得各個內存區域的總和大於物理內存限制(包括物理上的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。

學習jvm(一)--java內存區域