其他分享
首页 > 其他分享> > 笔记系列:JVM运行时数据区与JVM指令集

笔记系列:JVM运行时数据区与JVM指令集

作者:互联网

本文重点介绍JVM运行时数据区的整体概况,其中堆和方法区等比较复杂的会在GC的部分学习。另外本文还学习了JVM的指令集,涉及到的常用的一些指令,通过查看JVM规范手册,还确定每一个是如何使用,并与运行时数据区进行对应。

笔记系列。

关键字:运行时数据区,自增的字节码指令执行,局部变量表,栈帧,this,iadd,invoke指令

1、引言

一个java类的完整生命周期如下:

class文件 -> (loading,linking,initailizing)-> JVM -> run engine -> 运行时数据区 -> GC

2、运行时数据区

  1. PC,program counter,程序计数器。存放下一条指令位置,空间很小。虚拟机运行就是一个循环,不断去取PC中的位置,找到对应位置的指令然后执行,PC++,直到结束。
    1. 每个JVM线程都有它自己的PC寄存器。
    2. 任何时候,每条JVM线程都在执行一个独立方法的代码,被称作那条线程的当前方法。
    3. 如果方法不是native的,pc寄存器包含的JVM指令的地址将被执行。
  2. Heap,堆内存。GC垃圾收集的时候会重点学习。
    1. JVM拥有一个Heap堆,它是被所有的JVM线程所共享的。
    2. 堆是在JVM运行时,所有对象实例和被分配的数组所占内存的空间。
  3. stack
    1. JVM stacks
      1. 每个JVM线程都有一个私有的JVM栈,在线程被创建的时候,栈也随即被创建。
      2. 一个JVM栈里面保存着很多的栈帧。
      3. stack frames,每个栈帧对应每一个方法。
    2. Native method stacks,本地方法栈,通过JNI去调用,保存的是C/C++依据JVM规范编写的方法。
  4. Direct memory,直接内存,NIO。原来的IO方式是数据先进入操作系统内存,然后JVM在执行时要从操作系统内存将数据拷贝过来一份再进行处理。NIO的直接内存方式就是省去了拷贝的过程,提高效率。Zero Copy,零拷贝,JVM可以直接去访问操作系统内核储存空间,不需要再拷贝到JVM空间处理。
  5. Method area,包含Run-time constant pool。
    1. JVM拥有一个方法区,是被所有线程所共享的。
    2. 方法区保存着每一个类的结构。
    3. 方法区是一个规范,包括两个不同版本的方法区的实现:
      1. Perm Space,JVM<1.8,FGC不会清理。大小在启动的时候指定,不能改变。一旦动态类特多的时候,会溢出。字符串常量位于PermSpace,在<1.8的情况下。而>1.8的时候,String常量位于堆。
      2. Meta Space,JVM>=1.8,会触发FGC清理。如果不设定默认最大就是物理内存占满,当满了就会FGC清理。
    4. 运行时常量池是每个类或接口在运行时读取的Class文件中的constant_pool的内容。

总结一下,

线程A: PC、JVMStacks、NMStacks

线程B: PC、JVMStacks、NMStacks

线程C: PC、JVMStacks、NMStacks

共享的内容:Heap,Method Area(Perm/Meta Space)

3、栈帧

栈帧对应一个线程的一个方法的内容,用于方法的执行,包括方法执行过程中的变量的临时状态。同时栈帧也执行动态链接,方法的返回值以及分发异常。栈帧被包含在JVM栈中。每一个栈帧包括:

1、局部变量表,Local Variable Table。

2、操作数栈,Operand Stack。

3、动态链接,Dynamic Linking。指向运行时常量池中的符号连接,如果解析就直接使用,未解析则执行解析再使用。例如A方法调用B方法,B要去常量池去找,找的这个过程就是动态链接。

4、返回地址,return address。a()->b(),方法a调用了方法b,b方法执行完了以后,返回值存放的位置,以及b方法执行完毕,应该接着执行a的哪里,也存放在这个返回地址。

3.1 自增代码的字节码检查

image-20220604194423912

这里面的局部变量表,如上图的选中部分,Class文件中的方法的code中的LocalVariableTable会被JVM读取进入每一个线程中的栈的一个栈帧中的局部变量表,可以理解为这是一对一的。

注意,上图中局部变量表是初始状态,最右侧选中的部分,显示的是int a,其中a是名字,int是描述符I来代替。

当前这个LocalVariableTable读取到JVM是初始状态,接下来在JVM中要执行code的JVM指令,通过这些指令的执行,会改变这个LocalVariableTable。下面先来看对应main方法源码code的JVM指令。

0 sipush 311
3 istore_1
4 iload_1
5 iinc 1 by 1
8 istore_1
9 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
12 iload_1
13 invokevirtual #3 <java/io/PrintStream.println : (I)V>
16 return

以上两句就完成了int a = 1;语句。

以上就完成了i++的代码,局部变量表的状态发生了变化。

以上完成的是i = i++;的代码,把i在操作数栈中值就赋回给局部变量表了。所以结果打印出来i仍旧等于311。后面的几行指令都是执行system.out.println()的指令的,不再深入介绍。

反过来,改为i=++i;进行查看code字节码。

0 sipush 311
3 istore_1
4 iinc 1 by 1
7 iload_1
8 istore_1
9 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
12 iload_1
13 invokevirtual #3 <java/io/PrintStream.println : (I)V>
16 return

这里面与a++的区别是 iinc 1 by 1和 iload_1指令的位置调换了,其他的都一样。

后面的逻辑是一样的,取出操作数栈栈顶的值保存到局部变量表下标1的值的位置中。

HotSpot的LocalVariableTable类似于CPU的寄存器,CPU寄存器的指令集是汇编语言。JVM的指令集是基于JVM规范,例如上面的字节码中code的存在于栈帧中的指令集,但硬件层面都会最终去执行CPU的寄存器,即执行汇编语言。

3.2 this

上面介绍了main方法的内容,那么栈帧与方法是一对一的,如果我们写一个自己的方法,它的字节码应该是怎样的呢?

image-20220604204046901

我们自己写了一个方法getMoney,但是要注意的是这个方法不是static的。可以看到它的字节码中方法的code的局部变量表有3行,其中第一行是this,指向了当前类。如果加上static关键字,局部变量表中就没有this了。

原因是什么呢?因为非static方法是需要对象来执行的,而对象的类被this所指定了。

3.3 iadd

image-20220604204852508

看一下code的JVM指令:

0 iload_1
1 iload_2
2 iadd
3 istore_3
4 iload_3
5 ireturn

3.4 类内部构造其他对象

image-20220604210817566

注意:前面提到了Stack Overflow。我们观察一下上图,在字节码方法的区域,有[0,1,2]三个方法,分别是init、main和add,这是Class文件中字节码代表的方法。要注意区分的是Class中的方法和JVM栈帧的区别,前者是静态的,后者是动态的,JVM栈帧在执行的时候会根据调用的层次关系逐个入栈,如果某方法并未被调用,则不会进入JVM栈帧。Class文件中每个方法静态的给出了code,这是对源码的翻译,也是JVM栈帧在执行时所需要解释执行的逐条读入的指令。

main方法中new了一个对象,下面重点看一下它的字节码指令。

0 new #2 <com/evswards/jvm/TestByteCodeJVM>
3 dup
4 invokespecial #3 <com/evswards/jvm/TestByteCodeJVM. : ()V>
7 astore_1
8 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
11 aload_1
12 iconst_1
13 iconst_2
14 invokespecial #5 <com/evswards/jvm/TestByteCodeJVM.add : (II)I>
17 invokevirtual #6 <java/io/PrintStream.println : (I)V>
20 return

后面的指令就不继续研究了,主要想体现的内容是对象引用的复制和调用时取走的机制。

3.5 返回值的接收

image-20220604213042821

main方法中创建当前类对象,然后调用m方法,m方法有返回值,但是main方法并没有派变量来接收。观察右侧code的字节码指令。前面的都学习过了,直接看pop。

下面看另一种情况:

image-20220604213420125

源码中我们做了调整,安排了int i来接收m的返回值。这时候code字节码的指令中发生了变化,原来pop的位置编程了istore_2,是因为main方法中i是在局部变量表中的下标为2的位置,前面还有0是args,1是h。所以将m的返回值在栈顶的内容直接出栈并写入到局部变量表中i的值里面。

回顾一下,this在局部变量表中什么时候出现,一定是在非static方法的局部变量表中的第一个元素。

3.6 递归

image-20220604214340522

编写了一个阶乘的实现。字节码指令解释一下没见过的:

3.7 Stack Overflow

栈帧本身位于JVM栈中,针对每一个方法有一个栈帧,上面例子中如果main方法调用了add方法,JVM栈中会先读取main方法栈帧,然后在执行到调用add方法时,会继续入栈,读取add方法的栈帧。此时JVM栈中会有两个栈帧同时存在。当add方法执行完毕,会按照add方法栈帧的Return Address返回到main方法栈帧之前执行中断的位置继续执行,而同时add方法栈帧会出栈销毁(执行完毕没有用了)。

那么当方法嵌套调用太多,会导致JVM栈中的栈帧太多,超过了栈大小的限制,就会报错Stack Overflow。这部分在GC内容中会详细学习。

4、invoke指令

上面有介绍到一些invoke指令,例如invokeVirtual、InvokeStatic等。JVM的invoke指令总共有:

4.1 InvokeDynamic

image-20220605125102079

lambda表达式的动态产生的类,会用到InvokeDynamic。输出的情况,看到都是Lambda的动态产生的类的情况。看一下字节码的内容:

image-20220605125225840

可以验证到,字节码的指令都是用到了InvokeDynamic。

JVM规范中还有很多没有涉及到的指令,可以在用到的时候去手动查询,重点关注操作数栈的变化以及执行的描述。

更多文章请转到一面千人的博客园

标签:JVM,局部变量,笔记,指令,指令集,执行,方法,栈帧
来源: https://www.cnblogs.com/Evsward/p/16343810.html