JVM第九天-运行时数据区与JVM操作指令集

在这里插入图片描述
前面我们说了一个class被加载到JVM中的具体过程,它包含 load,link,initialize。
在此之后,被加载到JVM后,将被执行引擎进行执行,这时候就涉及到了JVM运行时的一些数据区分布了。

运行时数据区分布

在这里插入图片描述

PC 程序计数器(Program Counter)

它用于存放指令位置。
虚拟机的运行,类似于这样的循环:

while( not end ) {
	取PC中的位置,找到对应位置的指令;
	执行该指令;
	PC ++;
}

在这里插入图片描述
每一个JVM中的线程都有它自己的一个PC(程序计数器)
如果一个方法不是本地方法(非native的),这个PC将会记录当前处理到的JVM指令集的位置。

JVM Stacks(栈)
在这里插入图片描述
每个Java虚拟机线程都有一个私有的Java虚拟机堆栈,它与线程同时创建。
Java虚拟机堆栈存放着一个个栈帧。栈帧是啥?这是我们的重点,一会再说。

Heap(堆)
在这里插入图片描述
堆是线程之间共享的一块区域。
所有类实例和数组的内存都是从堆这里进行分配的。

Method Area(方法区)
在这里插入图片描述

方法区也是所有的JVM线程共享的一块区域。
它存储着所有的Class。
方法区可以认为是一个接口(一种规范),而对它的实现有2种:
1、Perm Space(永久代)
在JDK1.8之前,方法区的实现是它。 FGC几乎不会清理这块空间,且这块区域只能在JVM启动的时候进行参数指定,之后在运行时就不能变了
2、Meta Space(元空间)
在JDK1.8开始,方法区的实现是它。 可以被FGC回收,且直接依赖的是物理内存,大小调控比较灵活,不设定的话就默认是系统最大物理内存。

关于字符串常量
在JDK1.8之前,字符串常量是存放在永久代的。从1.8开始,字符串常量被转移到了堆中存储。

方法区中,还包含了一块区域,叫运行时常量池:

Run-Time Constant Pool(运行时常量池)

在这里插入图片描述

运行时常量池是class中常量池表的每个类或每个接口的运行时表示

Native Method Stacks(本地方法栈)

在这里插入图片描述
这个结构和JAVA方法栈相同,但是只在通过JNI调用一些本地方法(由C,C++编写的实现方法)时进行存储栈帧的。

Direct Memory(直接内存,也叫堆外内存)

为了提高IO传输效率,在JDK1.4之后,JVM可以直接访问的内核空间的内存 (OS 管理的内存)
NIO就是大量利用了这块内存来提高效率,这也是实现zero copy的基础。

一张图概述区域分布

在这里插入图片描述

这张图可以比较清晰的概述上面的分布,可以看到,每个线程独有自己的PC,方法栈,本地方法栈。 而共享堆区和方法区。

在这里插入图片描述

这是因为线程间切换,切换回来的时候需要使用PC来定位到之前线程指令执行到的位置。

栈帧

在这里插入图片描述
上面我们说到,在JVM中,每个线程会独享一个JAVA方法栈,而栈中会可能存在多个栈帧。

而这一个个栈帧怎么来的?

实际上,我们的每个方法都会分别对应一个栈帧。

一个栈帧存储着四部分的东西:

Local Variable Table(局部变量表)

一个方法内入参和声明的局部变量,都是存放到我们局部变量里。
如果一个方法是非静态方法,那么还额外会将“this”放入表中。

Operand Stacks(操作数栈)

方法代码块内的每一行代码都会被解释成一条或多条指令,放入到操作数栈当中。

Dynamic Linking(动态链接)

比如在a方法里调用了方法b,在代码块中被虚拟机解释成操作码后,这个操作码会指向了一个常量池中关于方法b的常量符号引用,如果这个符号引用没有被解析成直接引用过,则对它进行替换成直接引用。
参考文章:https://blog.csdn.net/qq_41813060/article/details/88379473

Return Address(返回地址)

a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方,以及执行完b后,需要回到哪个位置继续执行。

通过程序来观察栈帧

public static void main(String[] args) {
    int i = 10;
    //i = i++;
    i = ++i;
    System.out.println(i);
}

public void test(String name){
    int b=2;
}

我们依然是使用jclasslib来观察:

局部变量表

这个程序的静态main方法里的栈帧中局部变量表里应该存放了几个变量呢?
在这里插入图片描述
可以看到,是2个。 印证了我们说的,入参和定义的变量。

非静态的test方法呢?来看看:
在这里插入图片描述

有没有发现什么不同:除了入参和声明的变量之外,发现在0号位上多了一个名称为“this”的变量。
没错,实例方法在局部变量表会多一个this的引用。

操作数栈

这个明面上不太好直接显示出来,但我们可以从观察一个方法的指令开始入手:

写法1:

public static void main(String[] args) {
    int i = 10;
    i = i++;
    System.out.println(i);
    
}

还是以main方法为例,观察它生成的操作指令:

 0 bipush 10
 2 istore_1
 3 iload_1
 4 iinc 1 by 1
 7 istore_1
 8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return

我们来一条条java语句和操作指令进行对应拆分讲解:

int i = 10;
//生成指令:
 0 bipush 10   向操作数栈 压栈 byte 10 并将10转换为一个int类型的值
 2 istore_1 将10出栈存入 为局部变量表为1号(i)的值

i = i++;
//生成指令:
 3 iload_1 从局部变量表i中加载值压入操作栈,此时值为104 iinc 1 by 1 对局部变量表的i的值+1,此时局部变量的i的值为11
 7 istore_1 将操作栈中的10重新出栈存入局部变量的i    此时,局部变量的i值重新变为10

System.out.println(i);
//生成指令:
 8 getstatic #2 <java/lang/System.out>
11 iload_1  从局部变量i中加载值压入操作栈,此时值为10
12 invokevirtual #3 <java/io/PrintStream.println>
15 return

:

写法2:

public static void main(String[] args) {
    int i = 10;
    i = ++i;
    System.out.println(i);
    
}

生成指令:

 0 bipush 10
 2 istore_1
 3 iinc 1 by 1
 6 iload_1
 7 istore_1
 8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return

我们来一条条java语句和操作指令进行对应拆分讲解:

/**
 * 以上的程序会输出的JVM操作指令如下:
 *         int i = 10;
 *  0 bipush 10   向操作数栈 压栈 byte 10
 *  2 istore_1 将10出栈存入 为局部变量为1号(i)的值
 *
 *
 *   i = ++i;
 *  3 iinc 1 by 1 对局部变量的i的值+1,此时局部变量的i的值为11
 *  6 iload_1 从局部变量i中加载值压入操作栈,此时值为11,
 *  7 istore_1 将操作栈中的10重新出栈存入局部变量的i   ,依旧是11
 *
 *   System.out.println(i);
 *
 *  8 getstatic #2 <java/lang/System.out>
 * 11 iload_1  从局部变量i中加载值压入操作栈,此时值为10
 * 12 invokevirtual #3 <java/io/PrintStream.println>
 * 15 return
 */

程序3:
在这里插入图片描述

程序4:
在这里插入图片描述

程序5:
在这里插入图片描述

程序6:
在这里插入图片描述
iadd,把栈顶两个int数弹出栈,进行运算,然后结果重新放入栈顶

程序7:
在这里插入图片描述
dup,复制一份栈顶的内容,然后压栈
这里dup的复制属于小细节,因为 invokespecial init这个指令 会调用构造方法,且消耗一个栈顶的引用,因此专门复制出一个用于调用构造使用,调用完毕后,原来的那个引用指向的对象就是初始化完毕的了。
这里也可以看到,一个对象的初始化是分成了好多步的。
还有个小细节:这里调用的构造方法可以看到是加了“”来I标识的,这说明是调用的一个实例方法构造。
除此之外,还有“”,这代表一个Class的静态初始化块的调用。

方法调用返回值

a()->b(),b方法会把返回值放于a方法的栈帧的栈顶
实例程序:
在这里插入图片描述

递归调用

方法的递归调用实际上也是一个栈帧一个栈帧垒上去。
每次方法都是一个栈帧,而递归实际和调用其他方法没什么不同,只不过就是调用自己方法,调用过程是一样的。
在这里插入图片描述

就上面这个程序来分析下这个过程:
我们只分析m方法的运行指令过程:

 0 iload_1 //读取局部变量表中的1号变量压栈,因为这是个实例方法,所以0号是this,1号是入参 n
 1 iconst_1 //将常量“1”压栈
 2 if_icmpne 7 (+5) //将栈中放入的两个值出栈,并进行比较,如果不相同,则跳到指令7
 5 iconst_1 //将常量“1”压栈
 6 ireturn //将栈顶数返回
 7 iload_1 //读取局部变量表中的1号变量压栈,因为这是个实例方法,所以0号是this,1号是入参 n
 8 aload_0 //读取局部变量表中的0号变量压栈,因为这是个实例方法,所以0号是this ,以便后续调用它自身的m方法
 9 iload_1 //读取局部变量表中的1号变量压栈,因为这是个实例方法,所以0号是this,1号是入参 n
10 iconst_1 //将常量“1”压栈
11 isub //将栈顶开始取2个数出栈,进行减法操作, 这里就是 n-1  并将结果压入栈顶
12 invokevirtual #4 <com/peng/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_04.m> //调用自身m方法,生成新栈帧,在新栈帧内执行完毕后,返回结果压入当前栈顶
15 imul //将栈顶开始取2个数出栈,进行乘法操作,并将结果压入栈顶
16 ireturn //将栈顶数返回

你可能观察到,这些指令的前面的索引号,似乎不是规律+1递增的,有的地方会跳好几个,这是因为有的指令自身比较大,存储需要多占了几个位置。

常用指令集

store

将栈顶值压入局部变量表

load

将局部变量表某个值压入栈顶

pop

从栈顶弹出某个值

mul

从栈中拿出2个数进行乘法运算

sub

从栈中拿出2个数进行除法运算

invoke方法:

InvokeStatic

调用静态方法的指令

public static void main(String[] args) {
    m();
}

public static void m() {}
InvokeVirtual

调用实例方法的指令,自带多态,会根据栈中的this去调用对应的具体实现方法。

public static void main(String[] args) {
    new T02_InvokeVirtual().m();
}

public void m() {}
InvokeInterface

以接口形式去调用方法的指令

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("hello");
}
InovkeSpecial

可以直接定位,不需要多态的方法
例如private 方法 , 构造方法

public static void main(String[] args) {
    T03_InvokeSpecial t = new T03_InvokeSpecial();
    t.n();
}

private void n() {}
InvokeDynamic

JVM最难的指令
lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令

public static void main(String[] args) {


    I i = C::n; //每次声明,java内部都会动态生成内部实现类
    I i2 = C::n;//每次声明,java内部都会动态生成内部实现类
    I i3 = C::n;//每次声明,java内部都会动态生成内部实现类
    System.out.println(i.getClass());
    System.out.println(i2.getClass());
    System.out.println(i3.getClass());
    //for(;;) {I j = C::n;}   //但实际上,for循环中的lambda声明似乎会被优化,多次循环的lambda实际只会生成一个对象。
}

@FunctionalInterface
public interface I {
    void m();
}

public static class C {
    static void n() {
        System.out.println("hello");
    }
}

打印结果:

class com.peng.run_time_data_area.T05_InvokeDynamic$$Lambda$1/1078694789
class com.peng.run_time_data_area.T05_InvokeDynamic$$Lambda$2/1023892928
class com.peng.run_time_data_area.T05_InvokeDynamic$$Lambda$3/558638686

指令集分类

1、基于寄存器的指令集

Hotspot中的局部变量表类似于 JVM中的寄存器

2、基于栈的指令集

操作数栈其实类似于基于栈的指令集

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值