JVM内存模型

Java虚拟机(JVM)内存模型是Java运行时数据区的一种规范,它定义了Java虚拟机在执行Java程序时如何使用内存。了解JVM内存模型对于优化Java应用程序、提高性能、避免内存泄漏和解决内存溢出问题至关重要。本文将以JDK8为例,详细解析JVM内存模型的各个组成部分。

JVM内存模型

JVM内存模型主要包括以下几个运行时数据区:方法区、堆、 栈、本地方法栈、程序计数器.
JVM内存模型_第1张图片

示例

下面Math.class对代码进行反汇编,结合Math类讲述模型各个部分.

public class Math {
    public static final int initData = 1;
    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();
    }
}

使用javap -c Math.class 反编译这段代码

Compiled from "Math.java"
public class myself.jvm.Math {
  public static final int initData;

  public static myself.jvm.User user;

  public myself.jvm.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()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 myself/jvm/Math
       3: dup
       4: invokespecial #3                  // Method "":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method compute:()I
      12: pop
      13: return

  static {};
    Code:
       0: new           #5                  // class myself/jvm/User
       3: dup
       4: invokespecial #6                  // Method myself/jvm/User."":()V
       7: putstatic     #7                  // Field user:Lmyself/jvm/User;
      10: return
}
// 拿compute()方法举例解释各行代表的意思 
public int compute();
    Code:
       0: iconst_1 // 将int类型常量1压入操作数栈
       1: istore_1 // 将int类型值存入局部变量1
       2: iconst_2 // 将int类型常量2压入栈
       3: istore_2 // 将int类型值存入局部变量2
       4: iload_1 // 从局部变量1中装载int类型值
       5: iload_2 // 从局部变量2中装载int类型值
       6: iadd // 执行int类型的加法
       7: bipush        10 // 将一个8位带符号整数压入栈,这里是10
       9: imul //  执行int类型的乘法
      10: istore_3 // 将int类型值存入局部变量3
      11: iload_3 // 从局部变量3中装载int类型值
      12: ireturn // 从方法中返回int类型的数据

程序员计数器(Program Counter Register)

程序计数器是当前线程所执行的字节码的行号指示器。在JVM的执行过程中,程序计数器用于记录下一条指令的执行地址。每个线程都有自己的程序计数器,是线程私有的内存。

// 作用
存储每行代码执行的位置,可理解为上述Math类反汇编代码中的行数.
// 原因 
由于多线程,CPU资源争夺,当线程A执行一段代码后,CPU资源被线程B占有,B执行完,A重新获取CPU后,会根据程序计数器记录的代码行号继续执行之后的代码.

方法区(Method Area)

方法区是所有线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK8中,传统的永久代(PermGen)已被元空间(Metaspace)所取代。元空间使用本地内存,因此,JDK8的方法区主要指的是元空间。

// 存储内容
常量、静态变量、类信息(.class).
// 方法区和堆的联系
若常量或静态变量是对象,则存储的是对象地址。例如Math类中的常量initData,静态对象User.

线程栈(Java Stack)

线程栈是线程私有的,它的生命周期与线程相同。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的调用到执行完成的过程,就对应着一个栈帧在Java栈中的入栈到出栈过程。

// 线程栈,存放局部变量
// 上图结合Math.class查看,可以获知
1.每当线程执行一个方法,虚拟机就会在栈里给该线程分配内存空间.FILO(先进后出,和程序调用顺序吻合).
2.一个方法对应一个栈帧,上图(main()、compute()分别分配了一块栈帧空间)jk
3.栈帧组成(结合compute()方法javac编译结果理解)
  a.局部变量表:方法局部变量a;如果局部变量是对象,存的是对象在堆的内存地址.
  b.操作数栈:程序在运行时,临时存放数值的中转空间.
  c.动态链接:在程序运行时,把符合引用转化为符号的直接引用代码。例如math.compute(); -> 对应的.CLASS里的代码
  d.方法出口:方法执行完后,返回指定的代码位置.
4.如果局部变量是对象,则存储的是对象地址,所以栈和堆是有联系的.

本地方法栈(Native Method Stack)

本地方法栈与Java栈类似,但它是为虚拟机使用到的Native方法服务。

堆(Heap)

堆是JVM内存模型中最大的一块,也是被所有线程共享的区域。它用于存储对象实例和数组。堆内存是垃圾收集器管理的主要区域,因此也被称为GC堆(Garbage-Collected Heap)。根据对象的存活周期,堆内存可以进一步细分为新生代(Young Generation)和老年代(Old Generation)。

JVM创建对象过程

JVM创建对象的过程涉及几个关键步骤,这些步骤确保了对象能够被正确地分配内存并初始化。以下是JVM创建对象的主要过程:
JVM内存模型_第2张图片

// 1.类加载检查
当代码中使用new关键字创建对象时,JVM首先会检查这个对象所属的类是否已经被加载、链接和初始化。
如果没有,那么必须先执行相应的类加载过程。
// 2.分配内存
一旦确定类已经被加载,JVM接下来会为新对象分配内存。
对象所需的内存大小在类加载完成后便已确定,内存分配的方式主要有两种:
a.指针碰撞
如果Java堆内存是绝对规整的,那么JVM只需移动一个指针,即可为对象分配内存.
这种情况下,所有用过的内存放在一边,空闲的内存放在另一边,中间是一个指针作为分界点,分配内存就是将这个指针向空闲空间移动对象大小的距离。
b.空闲列表
如果Java堆内存不是规整的,JVM需要通过维护一个列表来记录哪些内存块是可用的。
分配内存时,JVM会从列表中找到一块足够大的空间分配给对象,并更新列表上的内容。

- 分配过程中存在线程安全问题
为了线程安全,内存分配时可能会采用CAS配上失败重试、本地线程分配缓冲(TLAB)等机制来保证更新操作的原子性。

// 3.初始化
内存分配完成后,JVM会将分配到的内存空间都初始化为零值(不包括对象头),
这一步确保了对象的实例字段在Java代码中可以不赋初值就直接使用。

// 4.设置对象头
JVM需要在对象的内存中存储对象头(Object Header,这部分信息包含了对象自身的运行时数据,
如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息。对于数组对象,还需要记录数组的长度。

// 5.执行方法
最后,JVM会执行对象的构造函数。构造函数中不仅包含了程序员编写的各种字段赋值操作,还可能包含编译器自动加入的对父类构造函数的调用。
执行构造函数的过程是对对象进行初始化,确保对象按照程序员的意图正确构造。

你可能感兴趣的:(jvm)