JVM運行時數據區(Run-TimeDataAreas)及內存結構


運行時數據區:

  在類加載階段的第2,3步可以涉及有運行時數據,堆,方法區等名詞。( 2.將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。3.在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口 )

  說白了就是類文件被類裝載器裝載進來之后,類中的內容(比如變量,常量,方法,對象等)這些數據得要有個去處,也就是要存儲起來,存儲的位置肯定是在JVM中有對應的空間。

  oracle官網介紹:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5  。Java虛擬機定義了在程序執行期間使用的各種運行時數據區域。其中一些數據區域是在Java虛擬機啟動時創建的,只有在Java虛擬機退出時才會銷毀。其他數據區域是每個線程。每個線程的數據區域在線程創建時創建,在線程退出時銷毀。關於運行時數據區可以用以下圖形來表示:

Method Area(方法區):

  方法區域是在虛擬機啟動時創建的。雖然方法區域在邏輯上是堆的一部分,但簡單的實現可能選擇不進行垃圾收集或壓縮。此規范並不強制要求方法區域的位置或用於管理已編譯代碼的策略。方法區域可以是固定大小的,也可以根據計算的需要進行擴展,如果不需要更大的方法區域,則可以收縮。方法區域的內存不需要是連續的。方法區是各個線程共享的內存區域,在虛擬機啟動時創建。用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻又一個別名叫做Non-Heap(非堆),目的是與Java堆區分開來。Java虛擬機實現可以為程序員或用戶提供對方法區域初始大小的控制,在可變大小方法區域的情況下,還可以提供對最大和最小方法區域大小的控制。以下是與方法區域相關的異常情況:如果方法區域中的內存不能滿足分配請求,則Java虛擬機拋出

OutOfMemoryError。

  此時回過頭思考一下類加載階段的第二步:將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構,就是將類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據存放到方法區。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池,用於存放編譯時期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。值得一提的是方法區在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space。

Heap(堆):

  Java堆是Java虛擬機所管理內存中最大的一塊,在虛擬機啟動時創建,被所有線程共享。Java對象實例以及數組都在堆上分配。對象的堆存儲由自動存儲管理系統(稱為垃圾收集器)回收;對象從不顯式釋放。Java虛擬機假設沒有特定類型的自動存儲管理系統,可以根據實現者的系統需求選擇存儲管理技術。堆的大小可以是固定的,也可以根據計算的需要進行擴展,如果不需要更大的堆,則可以收縮。堆的內存不需要是連續的。Java虛擬機實現可以為程序員或用戶提供對堆初始大小的控制,如果可以動態擴展或收縮堆,還可以控制堆的最大和最小大小。

  此時回看類加載階段的第3步:在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口,就是將這個對象分配到堆區。

Run-Time Constant Pool(運行時常量池):

  運行時常量池是類文件中constant_pool表的每個類或每個接口的運行時表示。它包含幾種類型的常量,從編譯時已知的數值常量到必須在運行時解析的方法和字段引用。運行時常量池的功能類似於傳統編程語言的符號表,盡管它包含的數據范圍比典型的符號表更廣。每個運行時常量池都是從Java虛擬機的方法區域中分配

。類或接口的運行時常量池是在Java虛擬機創建類或接口時構造的。在創建類或接口時,如果構建運行時常量池所需的內存超過了Java虛擬機的方法區域所能提供的內存,則Java虛擬機將拋出OutOfMemoryError。

The pc Register(程序計數器):

  程序計數器占用的內存空間很小,由於Java虛擬機的多線程是通過線程輪流切換,並分配處理器執行時間的方式來實現的,在任意時刻,一個處理器只會執行一條線程中的指令。因此,為了線程切換后能夠恢復到正確的執行位置,每條線程需要有一個獨立的程序計數器(線程私有)。如果線程正在執行Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,則這個計數器為空。

Native Method Stacks(本地方法棧):

  如果當前線程執行的方法是Native類型的,這些方法就會在本地方法棧中執行。

Java Virtual Machine Stacks(虛擬機棧):

  經過上面的分析,類加載機制的裝載過程已經完成,后續的鏈接,初始化也會相應的生效。每個java方法在執行時,會創建一個“棧幀(stack frame)”,棧幀的結構分為“局部變量表、操作數棧、動態鏈接、方法出口”幾個部分。虛擬機棧是一個線程執行的區域,保存着一個線程中方法的調用狀態。換句話說,一個Java線程的運行狀態,由一個虛擬機棧來保存,所以虛擬機棧肯定是線程私有的,獨有的,隨着線程的創建而創建。每一個被線程執行的方法,為該棧中的棧幀,即每個方法對應一個棧幀。調用一個方法,就會向棧中壓入一個棧幀;一個方法調用完成,就會把該棧幀從棧中彈出。圖示如下:

  每個棧幀中包括局部變量表(Local Variables)、操作數棧(Operand Stack)、指向運行時常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。

  1. 局部變量表:方法中定義的局部變量以及方法的參數存放在這張表中,局部變量表中的變量不可直接使用,如需要使用的話,必須通過相關指令將其加載至操作數棧中作為操作數使用。
  2. 操作數棧:以壓棧和出棧的方式存儲操作數的。
  3. 動態鏈接:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接(Dynamic Linking)。例如只有在程序運行的時候才能確定某變量的類型。
  4. 方法返回地址:執行到那了。當一個方法開始執行后,只有兩種方式可以退出,一種是遇到方法返回的字節碼指令;一種是遇見異常,並且這個異常沒有在方法體內得到處理。

  oracle官網的描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6 .其結構如下圖:

   下面我們可以通過一個簡單的例子來看一下虛擬機棧在線程執行方法得時候是怎么操作的。例如下面的方法及字節碼指令,可以通過javap -v XX.class 查看。javap -c XX.class  >XX.txt輸出到文件。

class Person{
    private String name="wuzz";
    private int age;
    private final double salary=100;
    private static String address;
    private final static String hobby="Programming";
    public void say(){
        System.out.println("person say...");
     }
    public static int calc(int op1,int op2){
        op1=3;
        int result=op1+op2;
        return result;
    }
    public static void order(){
   
     }
    public static void main(String[] args){
        calc(1,2);
        order();
    }
}    
//抽取calc這個方法進行舉例
public static int calc(int, int);
  Code:
   0: iconst_3   //將int類型常量3壓入[操作數棧]
   1: istore_0   //將int類型值存入[局部變量0]
   2: iload_0    //從[局部變量0]中裝載int類型值入棧
   3: iload_1    //從[局部變量1]中裝載int類型值入棧
   4: iadd     //將棧頂元素彈出棧,執行int類型的加法,結果入棧
   5: istore_2   //將棧頂int類型值保存到[局部變量2]中
   6: iload_2    //從[局部變量2]中裝載int類型值入棧
   7: ireturn    //從方法中返回int類型的數據

  可以看到字節碼指令分為了7步。至於具體每一步做了什么操作我們可以通過官網提供的表格進行查詢 https://docs.oracle.com/javase/specs/jvms/se8/html/index.html .

  其中字節碼的 0-4 步的相關操作如下圖:

  1. 如果在棧幀中有一個變量,類型為引用類型,比如Object obj=new Object(),這時候就是典型的棧中元素指向堆中的對象。
  2. 方法區中會存放靜態變量,常量等數據。如果是下面這種情況,private static Object obj=new Object(); 就是典型的方法區中元素指向堆中的對象。
  3. 堆指向方法區:方法區中會包含類的信息,堆中會有對象,那么對象是哪個類創建的,一個對象怎么知道它是由哪個類創建的,這些信息存儲在Java對象的內存布局具體信息里面。

  Java對象內存布局:

  一個Java對象在內存中包括3個部分:對象頭、實例數據和對齊填充

JVM 內存模型:

  一塊是非堆區,JVM用永久代(PermanetGeneration)來存放方法區,(在JDK的HotSpot虛擬機中,可以認為方法區就是永久代,但是在其他類型的虛擬機中,沒有永久代的概念)。一塊是堆區。堆區分為兩大塊,一個是Old區(老年代),一個是Young區(新生代)。Young區分為兩大塊,一個是Survivor(S0+S1),一塊是Eden區。 Eden:S0:S1=8:1:1S0和S1一樣大,也可以叫From和To。圖示如下:

  根據之前對於Heap的介紹可以知道,一般對象和數組的創建會在堆中分配內存空間,關鍵是堆中有這么多區域,那一個對象的創建到底在哪個區域呢?

  一般情況下,新創建的對象都會被分配到Eden區,一些特殊的大的對象會直接分配到Old區。比如有對象A,B,C等創建在Eden區,但是Eden區的內存空間肯定有限,比如有100M,假如已經使用了100M或者達到一個設定的臨界值,這時候就需要對Eden內存空間進行清理,即垃圾收集(Garbage Collect),這樣的GC我們稱之為Minor GC,Minor GC指得是Young區的GC。經過GC之后,有些對象就會被清理掉,有些對象可能還存活着,對於存活着的對象需要將其復制到Survivor區,然后再清空Eden區中的這些對象。

Survivor區詳解:

  由圖解可以看出,Survivor區分為兩塊S0和S1,也可以叫做From和To。在同一個時間點上,S0和S1只能有一個區有數據,另外一個是空的。接着上面的GC來說,比如一開始只有Eden區和From中有對象,To中是空的。此時進行一次GC操作,From區中對象的年齡就會+1,我們知道Eden區中所有存活的對象會被復制到To區,From區中還能存活的對象會有兩個去處。若對象年齡達到之前設置好的年齡閾值,此時對象會被移動到Old區, 如果Eden區和From區 沒有達到閾值的對象會被復制到To區。 此時Eden區和From區已經被清空(被GC的對象肯定沒了,沒有被GC的對象都有了各自的去處)。這時候From和To交換角色,之前的From變成了To,之前的To變成了From。也就是說無論如何都要保證名為To的Survivor區域是空的。Minor GC會一直重復這樣的過程,直到To區被填滿,然后會將所有對象復制到老年代中。

Old區詳解:

  從上面的分析可以看出,一般Old區都是年齡比較大的對象,或者相對超過了某個閾值的對象。在Old區也會有GC的操作,Old區的GC我們稱作為Major GC 。

  默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。

Java對象的一輩子:

  我是一個普通的Java對象,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,我就開始漂了,有時候在Survivor的“From”區,有時候在Survivor的“To”區,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。於是我就去了年老代那邊,年老代里,人很多,並且年齡都挺大的,我在這里也認識了很多人。在年老代里,我生活了20年(每次GC加一歲),然后被回收。下圖展現了一個對象的分配過程。

 

為什么需要Survivor區?只有Eden不行嗎?減少Full GC

  如果沒有Survivor,Eden區每進行一次Minor GC ,並且沒有年齡限制的話, 存活的對象就會被送到老年代。這樣一來,老年代很快被填滿,觸發Major GC(因為Major GC一般伴隨着Minor GC,也可以看做觸發了Full GC)。老年代的內存空間遠大於新生代,進行一次Full GC消耗的時間比Minor GC長得多。執行時間長有什么壞處?頻發的Full GC消耗的時間很長,會影響大型程序的執行和響應速度。可能你會說,那就對老年代的空間進行增加或者較少咯。假如增加老年代空間,更多存活對象才能填滿老年代。雖然降低Full GC頻率,但是隨着老年代空間加大,一旦發生FullGC,執行所需要的時間更長。假如減少老年代空間,雖然Full GC所需時間減少,但是老年代很快被存活對象填滿,Full GC頻率增加。所以Survivor的存在意義,就是減少被送到老年代的對象,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次(默認第16次就去老年代)Minor GC還能在新生代中存活的對象,才會被送到老年代。

為什么需要兩個Survivor區?去碎片化(就是整理一下,排排坐)

  最大的好處就是解決了碎片化。也就是說為什么一個Survivor區不行?第一部分中,我們知道了必須設置Survivor區。假設現在只有一個Survivor區,我們來模擬一下流程:剛剛新建的對象在Eden中,一旦Eden滿了,觸發一次Minor GC,Eden中的存活對象就會被移動到Survivor區。這樣繼續循環下去,下一次Eden滿了的時候,問題來了,此時進行Minor GC,Eden和Survivor各有一些存活對象,如果此時把Eden區的存活對象硬放到Survivor區,很明顯這兩部分對象所占有的內存是不連續的,也就導致了內存碎片化。永遠有一個Survivor space是空的,另一個非空的Survivor space無碎片。

新生代中Eden:S1:S2為什么是8:1:1?

  GC是統計學測算出當內存使用超過98%以上時,內存就應該被minor gc時回收一次。但是實際應用中,我們不能較真的只給 他們留下2%,換句話說當內存使用達到98%時才GC 就有點晚了,應該是多一些預留10%內存空間,這預留下來的空間我們稱為S區(有兩個s區  s1 和  s0),S區是用來存儲新生代GC后存活下來的對象,大多數的對象都是朝生夕死,生命周期短(大多是web應用,比如一個訂單下好了,就好了)。而我們知道新生代GC算法使用的是復制回收算法。所以我們實際GC發生是在,新生代內存使用達到90%時開始進行,復制存活的對象到S1區,要知道GC結束后在S1區活下來的對象,需要放回給S0區,也就是對調(對調是指,兩個S區位置互換,意味着再一次minor gc 時的區域 是eden 加上一次存活的對象放入的S區),既然能對調,其實就是兩個區域一般大。這也是為什么會再有個10%的S0區域出來。這樣比例就是8:1:1了 ,這里的eden區(80%) 和其中的一個  S區(10%) 合起來共占據90%,GC就是清理的他們,始終保持着其中一個  S  區是空留的,保證GC的時候復制存活的對象有個存儲的地方。

使用jvisualvm查看內存分配及內存溢出實戰:

  在${JAVA_HOME}/bin 目錄下找到 jvisualvm.exe 雙擊運行即可,這里需要安裝一個插件 com-sun-tools-visualvm-modules-visualgc.nbm 。下載地址 https://visualvm.github.io/pluginscenters.html--->選擇對應版本鏈接--->Tools--->Visual GC。

  堆內存溢出:

  隨便創建一個 springboot 項目,設置好參數比如-Xmx20M -Xms20M,如果本身項目里信息比較多,不能設置太小,會導致啟動失敗。然后加個接口(Person類隨便加兩個屬性即可,或者隨便一個對象):

@RestController
public class HeapController {
  List<Person> list=new ArrayList<Person>();
  @GetMapping("/heap")
  public String heap() throws Exception{
    while(true){
      list.add(new Person());
      Thread.sleep(1);
   }
 }
}

   然后打開jvisualvm進行查看如下,訪問上面這個接口:

   能非常直觀的感受到 Eden 區一次一次的進行GC,然后S0與S1 兩個區域來回對調,導致old區的對象啊越來越多最后導致對內存溢出:

  方法區內存溢出:

  從上面的介紹我們知道方法區主要存儲類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。那么我們就模擬添加類的元數據信息,不斷的創建區添加。需要添加asm工具進行模擬。asm依賴和Class代碼:

<dependency>
  <groupId>asm</groupId>
  <artifactId>asm</artifactId>
  <version>3.3.1</version>
</dependency>
public class MyMetaspace extends ClassLoader {   public static List<Class<?>> createClasses() {     List<Class<?>> classes = new ArrayList<Class<?>>();     for (int i = 0; i < 10000000; ++i) {       ClassWriter cw = new ClassWriter(0);       cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,           "java/lang/Object", null);       MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",           "()V", null, null);       mw.visitVarInsn(Opcodes.ALOAD, 0);       mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",           "<init>", "()V");       mw.visitInsn(Opcodes.RETURN);       mw.visitMaxs(1, 1);       mw.visitEnd();       MyMetaspace test = new MyMetaspace();       byte[] code = cw.toByteArray();       Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);       classes.add(exampleClass);    }     return classes;  } }
@RestController
public class NonHeapController {   List<Class<?>> list=new ArrayList<Class<?>>();   @GetMapping("/nonheap")   public String nonheap() throws Exception{     while(true){       list.addAll(MyMetaspace.createClasses());       Thread.sleep(5);    }  } }

   設置Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M .設置太小同樣會報錯哦:Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

   然后訪問接口,直接就報錯了:

   虛擬機棧StackOverFlow:

public class StackDemo {
  public static long count=0;
  public static void method(long i){
    System.out.println(count++);
    method(i);
 }
  public static void main(String[] args) {
    method(1);
 }
}

   通過上面這個代碼進行運行,無限遞歸進行壓棧:

 

    可以看到這里再7298的時候拋出了異常,可以通過-Xss128k:設置每個線程的堆棧大小。設置的越小 7298這個臨界數也越小。

  Stack Space用來做方法的遞歸調用時壓入Stack Frame(棧幀)。所以當遞歸調用太深的時候,就有可能耗盡Stack Space,爆出StackOverflow的錯誤。JDK 5以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。線程棧的大小是個雙刃劍,如果設置過小,可能會出現棧溢出,特別是在該線程內有遞歸、大的循環時出現溢出的可能性更大,如果該值設置過大,就有影響到創建棧的數量,如果是多線程的應用,就會出現內存溢出的錯誤。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM