JVM內存結構深度解析(一)

JVM的總體結構

這是Oracle官方對jvm內存的定義:docs.oracle.com/javase/spec…html

內存的分佈結構

  • 虛擬機棧:每一個線程獨有的,虛擬機會爲每一個線程都開闢一塊棧空間用來存放當前線程的每一個方法在執行過程當中的局部變量,固然,這裏不單單隻有局部變量,還有 操做數棧、動態鏈接、方法出口;
  • 本地方法棧:和虛擬機棧相似,可是他是存放的native方法在執行過程當中的局部變量,這部分不多用,不用去關注;
  • 程序計數器:用來記錄每一個線程在進行上下文切換的時候,記錄上次執行的符號引用,也是每一個線程獨有的;
  • 方法區:在JVM規範當中叫方法區,可是在hotspot的jdk8版本是使用元空間(Metaspace)實現的,因此也能夠叫作元空間,類在被加載的時候會把類元信息放到這裏、類的靜態變量、靜態常量也會放到這裏;
  • :堆是你們很熟悉的也是特別須要去關注的,存放的是new出來的對象,堆的內部結構主要有兩部分構成:新生代和老年代,默認佔比是1:2;新生代又分爲1個eden區,2個suvivor區,默認佔比爲8:1:1;

image.png

內存的分配過程

經過以下代碼生成class文件,詳細分析一下在棧裏面對象是怎麼劃分的:java

public class Math {
    public void compute() {
        int a = 10;
        int b = 5;
        int c = (a + b) * 100;
        System.out.println(c);
    }
    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}
複製代碼

執行 javap -c Math.class > Math.txt 對class文件進行反編譯並輸出到Math.txt文件裏面詳細看一下:web

Compiled from "Math.java"
public class com.demo.jvm.Math {
  public com.demo.jvm.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void compute();
    Code:
       0: bipush        10
       2: istore_1
       3: iconst_5
       4: istore_2
       5: iload_1
       6: iload_2
       7: iadd
       8: bipush        100
      10: imul
      11: istore_3
      12: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      15: iload_3
      16: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      19: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #4                  // class com/demo/jvm/Math
       3: dup
       4: invokespecial #5                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #6                  // Method compute:()V
      12: return
}
複製代碼

反編譯以後的結果就是jvm的指令碼,每條指令碼都有它特定的含義算法

咱們對compute()方法的指令一條一條的分析:markdown

  1. bipush 10 將一個8位帶符號整數壓入棧:把10放入操做數棧
  2. istore_1 將int類型值存入局部變量1:把操做數棧裏面的值放到局部變量表下標爲1的位置(這裏爲何是下標1而不是0呢?當棧幀被建立出來的時候,局部變量表中會默認存放一個this的引用,下標是0)
  3. iconst_5 將int類型常量5壓入棧:和第1步的意思同樣,把5放入操做數棧
  4. istore_2 將int類型值存入局部變量2:把操做數棧裏面的5放到局部變量表的下標爲2的位置
  5. iload_1 從局部變量1中裝載int類型值:把局部變量表中的10拿出來放到操做數棧裏面;
  6. iload_2 從局部變量2中裝載int類型值:把局部變量表中的5拿出來放到操做數棧裏面
  7. iadd 執行int類型的加法:把操做數棧裏面的值加起來(10 + 5)= 15
  8. bipush 100 將一個8位帶符號整數壓入棧:把100放入操做數棧
  9. imul 執行int類型的乘法:把操做數棧裏面的值乘起來(15 * 100)= 1500
  10. istore_3 將int類型值存入局部變量3:把操做數棧的1500放到局部變量表下標3的位置
  11. getstatic #2 從類中獲取靜態字段:這行指令是獲取PrintStream對象的,就是System.out那段代碼,後面的#2是符號引用,在常量池裏面維護;
  12. iload_3 從局部變量3中裝載int類型值:把1500從局部變量表3的位置拿出來放到操做數棧裏面
  13. invokevirtual #3 調度對象的方法:調用#3引用的對象的方法,#3也是符號引用,實際就是調用PrintStream.println參數就是操做數棧裏面的1500;
  14. return 方法執行完畢,return結束;
  • 操做數棧:其實就是執行引擎將要進行操做的值,操做完以後就清除掉了,再次操做須要從局部變量表去加載;
  • 局部變量表:就是要存放的局部變量的值(基本數據類型),若是是存放對象,則是對象在堆中的地址引用(這裏也涉及到直接指針的概念,後面的內容會說到);
  • 動態連接:代碼在執行的過程當中對應的符號引用轉換爲直接地址,且這個引用是會變的;上篇還說到了靜態連接,這裏要和靜態連接區分一下,靜態連接是在類加載的時候發生的,動態連接是在對象執行方法的時候發生的;

每一個方法都對應一個棧幀,若是無限遞歸則會致使StackOverFlowError,棧的默認大小爲1M併發

方法區

使用上面的Math代碼,執行javap -v Math.class 查看描述信息:oracle

Classfile /E:/workspace/learn/blog-demo/target/classes/com/demo/jvm/Math.class
  Last modified 2021-5-19; size 773 bytes
  MD5 checksum 09a14d9236f6ceccf06184daa6cef7d9
  Compiled from "Math.java"
public class com.demo.jvm.Math minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref          #8.#29         // java/lang/Object."<init>":()V
   #2 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #32.#33        // java/io/PrintStream.println:(I)V
   #4 = Class              #34            // com/demo/jvm/Math
   #5 = Methodref          #4.#29         // com/demo/jvm/Math."<init>":()V
   #6 = Methodref          #4.#35         // com/demo/jvm/Math.compute:()V
   #7 = Fieldref           #4.#36         // com/demo/jvm/Math.math:Lcom/demo/jvm/Math;
   #8 = Class              #37            // java/lang/Object
   #9 = Utf8               math
  #10 = Utf8               Lcom/demo/jvm/Math;
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               compute
  #18 = Utf8               a
  #19 = Utf8               I
  #20 = Utf8               b
  #21 = Utf8               c
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               <clinit>
  #27 = Utf8               SourceFile
  #28 = Utf8               Math.java
  #29 = NameAndType        #11:#12        // "<init>":()V
  #30 = Class              #38            // java/lang/System
  #31 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;
  #32 = Class              #41            // java/io/PrintStream
  #33 = NameAndType        #42:#43        // println:(I)V
  #34 = Utf8               com/demo/jvm/Math
  #35 = NameAndType        #17:#12        // compute:()V
  #36 = NameAndType        #9:#10         // math:Lcom/demo/jvm/Math;
  #37 = Utf8               java/lang/Object
  #38 = Utf8               java/lang/System
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               println
  #43 = Utf8               (I)V
{
  public com.demo.jvm.Math();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/demo/jvm/Math;

  public void compute();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: iconst_5
         4: istore_2
         5: iload_1
         6: iload_2
         7: iadd
         8: bipush        100
        10: imul
        11: istore_3
        12: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: iload_3
        16: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        19: return
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 5
        line 11: 12
        line 12: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Lcom/demo/jvm/Math;
            3      17     1     a   I
            5      15     2     b   I
           12       8     3     c I 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: new           #4                  // class com/demo/jvm/Math
         3: dup
         4: invokespecial #5                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #6                  // Method compute:()V
        12: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  args   [Ljava/lang/String;
            8       5     1  math   Lcom/demo/jvm/Math;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #4                  // class com/demo/jvm/Math
         3: dup
         4: invokespecial #5                  // Method "<init>":()V
         7: putstatic     #7                  // Field math:Lcom/demo/jvm/Math;
        10: return
      LineNumberTable:
        line 5: 0
}
SourceFile: "Math.java"
複製代碼

Constant pool部分就是這個類的常量池,這些信息都是存放在方法區裏面的,能夠看到,每一個指令都有對應的符號引用,這就是類元信息;使用的是直接內存,默認大小爲21M,容量滿了會觸發FullGC,這部分的內存會進行動態調整,若是上次GC回收了大量的內存,則會自動調小,若是沒有回收大量內存則會調大,可是最大不會超過設置的最大空間;app

通常狀況下,對象在被建立出來是存放在eden區,當eden區放滿了以後會觸發一次MinorGC,使用複製算法把剩餘對象放到其中一個Survivor區,繼續進行對象的 建立->清理,當年輕代的那些被GC了15次尚未回收的對象放入老年代,老年代通常狀況下在沒法存放對象的時候會進行一次FullGC(根據具體的垃圾回收器決定,CMS會經過一個併發清理的參數設置何時執行FullGC),此次GC的範圍包含年輕代,老年代,元空間;若是在老年代沒有回收出來可用空間則會直接拋出OOM。
能夠跑下面這個demo,使用jvisualvm工具查看對象的分配過程jvm

public class OOMTest {
    private byte[] bytes = new byte[1024 * 1024];
    
    public static void main(String[] args) {
        ArrayList<OOMTest> list = new ArrayList<>();
        for(;;) {
            list.add(new OOMTest());
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
複製代碼

本章相關jvm參數:

-XX:MetaspaceSize 元空間的默認大小
-XX:MaxMetaspaceSize 設置元空間的最大內存,若是不設置的話會一直擴容,直到直接內存溢出OOM,經驗值設置爲256M;
-Xss 每一個線程的棧大小,默認爲1M
-Xms 初始堆大小,默認物理內存的1/64
-Xmx 最大堆大小,默認物理內存的1/4
-Xmn 新生代大小
-XX:NewSize 設置新生代初始大小
-XX:NewRatio 默認2表示新生代佔年老代的1/2,佔整個堆內存的1/3。
-XX:SurvivorRatio 默認8表示一個survivor區佔用1/8的Eden內存,即1/10的新生代內存svg

路漫漫其修遠兮,吾將上下而求索

相關文章
相關標籤/搜索