JVM内存结构深度解析(一)

1,198 阅读8分钟

JVM的整体结构

这是Oracle官方对jvm内存的定义:docs.oracle.com/javase/spec…

内存的分布结构

  • 虚拟机栈:每个线程独有的,虚拟机会为每个线程都开辟一块栈空间用来存放当前线程的每个方法在执行过程中的局部变量,当然,这里不仅仅只有局部变量,还有 操作数栈、动态连接、方法出口;
  • 本地方法栈:和虚拟机栈类似,但是他是存放的native方法在执行过程中的局部变量,这部分很少用,不用去关注;
  • 程序计数器:用来记录每个线程在进行上下文切换的时候,记录上次执行的符号引用,也是每个线程独有的;
  • 方法区:在JVM规范当中叫方法区,但是在hotspot的jdk8版本是使用元空间(Metaspace)实现的,所以也可以叫做元空间,类在被加载的时候会把类元信息放到这里、类的静态变量、静态常量也会放到这里;
  • :堆是大家很熟悉的也是特别需要去关注的,存放的是new出来的对象,堆的内部结构主要有两部分构成:新生代和老年代,默认占比是1:2;新生代又分为1个eden区,2个suvivor区,默认占比为8:1:1;

image.png

内存的分配过程

通过如下代码生成class文件,详细分析一下在栈里面对象是怎么划分的:

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文件里面详细看一下:

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()方法的指令一条一条的分析:

  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 查看描述信息:

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回收了大量的内存,则会自动调小,如果没有回收大量内存则会调大,但是最大不会超过设置的最大空间;

一般情况下,对象在被创建出来是存放在eden区,当eden区放满了之后会触发一次MinorGC,使用复制算法把剩余对象放到其中一个Survivor区,继续进行对象的 创建->清理,当年轻代的那些被GC了15次还没有回收的对象放入老年代,老年代一般情况下在无法存放对象的时候会进行一次FullGC(根据具体的垃圾回收器决定,CMS会通过一个并发清理的参数设置什么时候执行FullGC),这次GC的范围包含年轻代,老年代,元空间;如果在老年代没有回收出来可用空间则会直接抛出OOM。
可以跑下面这个demo,使用jvisualvm工具查看对象的分配过程

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的新生代内存

路漫漫其修远兮,吾将上下而求索