# JVM实战:JVM运行时数据区包含哪几部分?

在这里插入图片描述

# JVM运行时数据区

Java虚拟机(JVM)在运行Java程序时,会把它管理的内存划分为若干个不同的数据区域。这就是JVM运行时数据区

我们可以把这些区域分为两大类:线程共享区(所有线程都能访问,生命周期与JVM相同)和线程私有区(每个线程独享,生命周期与线程相同)

在这里插入图片描述

线程共享区:堆和方法区

线程私有区:程序计数器,虚拟机栈,本地方法栈

# 线程私有区

这些区域随着线程的启动而创建,随着线程的结束而销毁。因为是线程私有的,所以不存在内存并发安全问题

# 程序计数器

角色:当前线程所执行的字节码的行号指示器

作用:JVM的执行引擎就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。在多线程轮流切换场景下,它用于记录线程切换回来后的执行位置

特点

  1. 内存空间很小
  2. 它是JVM规范中唯一一个没有规定任何 OutOfMemoryError(OOM)情况的区域

# 虚拟机栈

角色:主管Java方法运行的内存模型 内部结构:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧

  1. 局部变量表:存放编译期可知的各种基本数据类型(int, char, boolean等)和对象引用(reference)
  2. 操作数栈:主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
  3. 动态链接:指向运行时常量池中该栈帧所属方法的引用,为了支持方法调用过程中的动态连接
  4. 方法出口(返回地址):存放调用该方法的位置,以便方法结束时能够返回

异常情况

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError(比如无限循环)

如果栈容量允许动态扩展,当扩展时无法申请到足够内存,会抛出 OutOfMemoryError

# 本地方法栈

角色:本地方法栈(Native Method Stack)与虚拟机栈锁发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

# 线程共享区

这些区域在JVM启动时创建,被所有线程共享,是垃圾回收(GC)重点眷顾的肥沃土地

# Java堆

角色:JVM中最大的一块内存区域,几乎所有的对象实例和数组都要在这里分配内存

垃圾回收的核心区:由于是GC的主战场,堆内存通常会被进一步划分,经典的划分方式(Java 8及以前)包括

新生代(Young Generation):又分为 Eden 区、Survivor 0 (From) 区、Survivor 1 (To) 区

老年代(Old Generation)

注意:到了 Java 9 及之后的现代垃圾回收器(如 G1, ZGC),堆的划分变成了无数个独立区域(Region),但其存放对象实例的核心本质没变

异常情况:如果没有内存完成实例分配,并且堆也无法再扩展时,抛出 OutOfMemoryError: Java heap space

在这里插入图片描述

# 方法区

角色:用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据

演进历史(极其重要)

jdk1.8以前:方法区的实现被称为永久代(Permanent Generation),它使用的是JVM内部的堆内存。

jdk 1.8 及以后:废除了永久代,改用元空间(Metaspace)来实现方法区。元空间不在JVM堆内存中,而是直接使用本地物理内存(Native Memory)

在这里插入图片描述

# 附录

细化一下上面提到的内容

# 虚拟机栈

虚拟机栈存储当前线程运行方法所需要的数据,指令,返回地址。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表

存储存储局部变量,是一个定长为32位的局部变量空间。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用一个。引用类型(new出来的对象)如何存储?看下图

public int methodOne(int a, int b) {
    Object obj = new Object();
    return a + b;
}

在这里插入图片描述 如果局部变量是Java的8种基本基本数据类型,则存在局部变量表中,如果是引用类型。如String,局部变量表中存的是引用,而实例在堆中。

假如methodOne方法调用methodTwo方法时, 虚拟机栈的情况如下 在这里插入图片描述

当虚拟机栈无法再放下栈帧的时候,就会出现StackOverflowError,演示一下

public class JavaVMStackSOF {

    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length: " + oom.stackLength);
            throw e;
        }
    }
}

在idea中设置运行时的线程的堆栈大小为如下

在这里插入图片描述

-Xss 参数的作用是设置每个线程的堆栈大小 运行输出为

在这里插入图片描述

-Xss参数的值越大,打印输出的深度越大

操作数栈

接着解释一下操作数栈,还是比较容易理解的

有如下一个Test类

public class Test {

    public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    }

    public int getSum(int a, int b) {
        return a + b;
    }
}

用javap反编译一下,看一下getSum的字节码文件内容

javap -v Test
public int getSum(int, int);
  descriptor: (II)I
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=3, args_size=3 //操作数栈大小为2,本地变量表大小为3,入参有3个
       0: iload_1 // 局部变量1压栈
       1: iload_2 // 局部变量2压栈
       2: iadd // 栈顶2个元素相加,计算结果压栈
       3: ireturn
    LineNumberTable: // 指令与代码行数的偏移关系
      line 17: 0
    LocalVariableTable: // 局部变量表
      // 作用域开始位置,作用偏移长度,槽位,变量名,类型描述
      Start  Length  Slot  Name   Signature
          0       4     0  this   Lcom/javashitang/jvm/Test;
          0       4     1     a   I
          0       4     2     b   I

当Java类编译完成时,操作数栈,本地变量表的大小就已经确定了。

操作数栈的大小为2 本地变量表有3个参数,this,a,b。其中this对象是jvm隐式传递的哈 入参有3个,this(jvm隐式传递),a,b

LineNumberTable和LocalVariableTable我用jclasslib Bytecode viewer插件(查看字节码比较方便,我一般不用javap命令)来解释一下

在这里插入图片描述

可以看到Test类的第17行代码对应的是getSum方法指令的第一行

在这里插入图片描述

getSum方法有3个局部变量 this,作用范围在[Start PC, Start PC+Length],在局部变量表的第0个位置,类型为Test类

在这里插入图片描述

图示如下

在这里插入图片描述

假如getSum方法的入参是long,则局部变量表如下(64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用一个)

public long getSum(long a, int b) {
    return a + b;
}

在这里插入图片描述

注意看局部变量表b的位置从2变为3了,因为a变量从原来占用一个slot变为占用2个slot

在这里插入图片描述

以calc方法的执行演示一下程序计数器,操作数栈,局部变量表是如何协同工作的

public int calc() {
    int a = 100;
    int b = 200;
    int c = 300;
    return (a + b) * c;
}

calc方法的字节码如下

在这里插入图片描述

执行流程图示如下

请添加图片描述

可能有小伙伴们对指令的作用不太熟悉,我就简单介绍一下

一般情况下,指令的格式有如下2种形式

  1. 操作指令
  2. 操作指令 操作数

istore 100 将一个数值从操作数栈存储到局部变量表中的第100位 istore_1 将一个数值从操作数栈存储到局部变量表中的第1位

为什么istore_1不写成istore 1,或者将istore 100写成istore_100?

因为将一个数值从操作数栈存储到局部变量表中的第1位这个操作经常发生,如果用istore_1则会占用1个字节,如果用istore 1会占用2个字节,所以用istore_1,可以节省空间。同时一个字节表示的种类数有限(128个,Java中各种操作指令占用1字节),所以istore_<n>这种形式不能表示所有操作类型,只能一少部分指令用istore_<n>,其余的用istore n这种形式

所以你现在理解了上图中指令的偏移量不是连续的原因了吧!

与操作类型相关的指令,会在最开头表明操作的类型

在这里插入图片描述

bipush:将一个常量加载到操作数栈 istore:将一个数值从操作数栈存储到局部变量表 iload:将一个局部变量加载到操作栈 iadd:对操作数栈栈顶的2个只进行加法运算,并将结果重新存入操作数栈栈顶

动态链接

Java有些方法,类加载的过程中就能知道具体执行的逻辑,而有些需要在运行的过程中才能确定具体执行的逻辑(多态),这就是动态链接在起作用,具体的实现没太看懂,就不过多分析了。