JVM学习笔记04-JVM内存模型

220 阅读6分钟

JVM内存模型

废话不多说,给你上个图,想必你一定觉得眼熟吧

JVM内存模型.png

  • 类装载子系统:将.class字节码文件加载到内存中
  • 字节码执行引擎:JVM的核心组成之一,用于执行字节码里面的那些指令
  • 程序计数器:主要存储的是当前线程所执行的字节码的行号,主要用作程序控制流的指示器分支循环跳转异常处理线程恢复等基础功能
  • 栈:用于执行Java方法
  • 本地方法栈:用于本地方法
  • 堆:用于存放对象实例
  • 方法区:用于存放已被虚拟机加载的类型信息常量静态变量等数据
  • 直接内存:这一块不属于JVM内存模型中的一块,是本机直接内存,受到本机总内存大小以及处理器寻址空间限制

其中,程序计数器本地方法栈都是线程私有的,方法区是各线程共享的

从上面我们知道,程序计数器、栈、本地方法栈是线程私有的,也就是说,每开一个线程,都会对应为这个线程开辟这几块内存空间。如图所示:

JVM栈.png

上面这张图主要为你展示程序计数器、栈、本地方法栈的线程私有特性,以及栈内部的接口

  • 栈帧:在栈的内部,每个方法被执行时,都会创建一个栈帧,并压入栈,直接结束后则弹出栈,每个方法被调用到执行完毕的过程,就对应一个栈帧从入栈到出栈的过程
  • 局部变量表:主要存储编译器可予置的各种Java基本数据类型和对象引用,存储的是对象的指针
  • 操作数栈:栈结构实现,主要存储字节码执行过程中的操作数
  • 动态链接:指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接
  • 方法出口:用以恢复它的上层方法执行状态

下面我们通过一段代码,帮助你理解一下栈

package org.laugen.jvm;

public class Math {
    public static int intData = 666;
    public static final int constant = 520;
    public static User user = new User();

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}

当程序运行的时候,会经历以下一系列过程:

  • 通过类装载子系统将.class文件加载的内存中
  • 创建主线程的程序计数器、栈、本地方法栈
  • 执行到main()方法,创建main栈帧并压入栈
  • 执行到math.compute(),创建compute-栈帧并压入栈
  • 执行完compute()方法后,compute-栈帧弹出栈
  • 执行完main()方法,main-栈帧出栈
  • 该线程结束,销毁线程并回收开辟的内存空间

下面,我们通过compute()方法,来看看一个方法在执行过程中,栈帧里面各个内存区域是如何变化的。首先,我们通过javap -c Math.class命令对这段代码反编译一下,然后阅读一下compute()方法反编译后的字节码指令

Compiled from "Math.java"
public class org.laugen.jvm.Math {
  public static int intData;

  public static final int constant;

  public static org.laugen.jvm.User user;

  public org.laugen.jvm.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int compute();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class org/laugen/jvm/Math
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method compute:()I
      12: pop
      13: return

  static {};
    Code:
       0: sipush        666
       3: putstatic     #5                  // Field intData:I
       6: new           #6                  // class org/laugen/jvm/User
       9: dup
      10: invokespecial #7                  // Method org/laugen/jvm/User."<init>":()V
      13: putstatic     #8                  // Field user:Lorg/laugen/jvm/User;
      16: return
}

这是反编译出来的结果,我们就看compute()方法这段代码

public int compute();
Code:
   0: iconst_1
   1: istore_1
   2: iconst_2
   3: istore_2
   4: iload_1
   5: iload_2
   6: iadd
   7: bipush        10
   9: imul
  10: istore_3
  11: iload_3
  12: ireturn

这里我们看到,一共会执行12条指令,我们现在一条一条阅读,可以自行上网下载一份JVM的指令手册

  • 0: iconst_1:将int类型常量1压入操作数栈,此时操作数栈只有一个数值1
  • 1: istore_1:将int类型值存入局部变量1,把数值1从操作数栈中弹出,此时完成int a = 1;这行代码,局部变量表中添加变量a并且它的值为1
  • 2: iconst_2:将int类型常量2压入操作数栈,此时操作数栈只有一个数值2
  • 3: istore_2:将int类型值存入局部变量1,把数值2从操作数栈中弹出,此时完成int b = 2;这行代码,局部变量表中添加变量b并且它的值为2
  • 4: iload_1:从局部变量1中装载int类型值,这时变量a的值1会被压入操作数栈,此时操作数栈只有一个数值1
  • 5: iload_2:从局部变量2中装载int类型值,这时变量b的值2会被压入操作数栈,此时操作数栈有两个数值,从栈底到栈顶分别是1,2
  • 6: iadd:执行int类型的加法,将操作数栈里面的数值1和2弹出栈进行计算,得出结果为3,把3压入操作数栈,此时操作数栈只有一个数值3
  • 7: bipush 10:将一个8位带符号整数压入操作数栈,此时操作数栈有两个数值,从栈底到栈顶分别是3,10
  • 9: imul:执行int类型的乘法,将操作数栈里里面的数值3和10弹出栈进行计算,得出结果为30,把30压入操作数栈,此时操作数栈只有一个数值30
  • 10: istore_3:将int类型值存入局部变量3,把数值30从操作数栈中弹出,此时完成int c = (a + b) * 10;这行代码,局部变量表中添加变量c并且它的值为30
  • 11: iload_3:从局部变量3中装载int类型值,这时变量c的值30会被压入操作数栈,此时操作数栈只有一个数值30
  • 12: ireturn:从方法中返回int类型的数据,把数值30从操作数栈中弹出,作为方法的返回值返回

分析完上面的过程,你是否对局部变量表和操作数栈有了进一步了解呢?

补充两点:

  • main-栈帧的局部变量表中会存math这个变量,它的值是math这个对象在堆上的指针
  • user是一个静态变量,方法区中它的值是user这个对象在堆上的指针

下面这个图是堆的内存模型,GC我会专门来讲,因此这里不过多涉及GC。

JVM堆.png 大概说一下上面这张图,JDK8中,JVM的堆主要分成两大块,分别是年轻代和老年代,他俩的占比一般情况下是年轻代:老年代=1:2,年轻代又分为Eden区和两个Survivor区,他们的占比大概是E:S1:S2=8:1:1。一般情况下,年轻代的对象通过15次GC后还存在,则会进入老年代。

那么,你知道为什么JVM的堆会有年轻代和老年代之分吗?