本文重点介绍JVM运行时数据区的整体概况,其中堆和方法区等比较复杂的会在GC的部分学习。另外本文还学习了JVM的指令集,涉及到的常用的一些指令,通过查看JVM规范手册,还确定每一个是如何使用,并与运行时数据区进行对应。 笔记系列。 关键字:运行时数据区,自增的字节码指令执行,局部变量表,栈帧,this,iadd,invoke指令
一个java类的完整生命周期如下:
class文件 -> (loading,linking,initailizing)-> JVM -> run engine -> 运行时数据区 -> GC
总结一下, 线程A: PC、JVMStacks、NMStacks 线程B: PC、JVMStacks、NMStacks 线程C: PC、JVMStacks、NMStacks 共享的内容:Heap,Method Area(Perm/Meta Space)
栈帧对应一个线程的一个方法的内容,用于方法的执行,包括方法执行过程中的变量的临时状态。同时栈帧也执行动态链接,方法的返回值以及分发异常。栈帧被包含在JVM栈中。每一个栈帧包括:
1、局部变量表,Local Variable Table。
2、操作数栈,Operand Stack。
3、动态链接,Dynamic Linking。指向运行时常量池中的符号连接,如果解析就直接使用,未解析则执行解析再使用。例如A方法调用B方法,B要去常量池去找,找的这个过程就是动态链接。
4、返回地址,return address。a()->b(),方法a调用了方法b,b方法执行完了以后,返回值存放的位置,以及b方法执行完毕,应该接着执行a的哪里,也存放在这个返回地址。
这里面的局部变量表,如上图的选中部分,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
short
。添加整数常量(311)到当前线程栈的main方法栈帧的操作数栈的栈顶[进栈]。int
into local variable。把操作数栈顶的数(311)出栈,设定为局部变量表中下标为1的变量(a)的值(311)。以上两句就完成了int a = 1;语句。
int
from local variable。将局部变量表中下标为1的变量的int值压入操作数栈栈顶。以上就完成了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的寄存器,即执行汇编语言。
上面介绍了main方法的内容,那么栈帧与方法是一对一的,如果我们写一个自己的方法,它的字节码应该是怎样的呢?
我们自己写了一个方法getMoney,但是要注意的是这个方法不是static的。可以看到它的字节码中方法的code的局部变量表有3行,其中第一行是this,指向了当前类。如果加上static关键字,局部变量表中就没有this了。
原因是什么呢?因为非static方法是需要对象来执行的,而对象的类被this所指定了。
看一下code的JVM指令:
0 iload_1 1 iload_2 2 iadd 3 istore_3 4 iload_3 5 ireturn
int
from method。方法返回int结果。注意:前面提到了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
后面的指令就不继续研究了,主要想体现的内容是对象引用的复制和调用时取走的机制。
main方法中创建当前类对象,然后调用m方法,m方法有返回值,但是main方法并没有派变量来接收。观察右侧code的字节码指令。前面的都学习过了,直接看pop。
下面看另一种情况:
源码中我们做了调整,安排了int i来接收m的返回值。这时候code字节码的指令中发生了变化,原来pop的位置编程了istore_2,是因为main方法中i是在局部变量表中的下标为2的位置,前面还有0是args,1是h。所以将m的返回值在栈顶的内容直接出栈并写入到局部变量表中i的值里面。
回顾一下,this在局部变量表中什么时候出现,一定是在非static方法的局部变量表中的第一个元素。
编写了一个阶乘的实现。字节码指令解释一下没见过的:
栈帧本身位于JVM栈中,针对每一个方法有一个栈帧,上面例子中如果main方法调用了add方法,JVM栈中会先读取main方法栈帧,然后在执行到调用add方法时,会继续入栈,读取add方法的栈帧。此时JVM栈中会有两个栈帧同时存在。当add方法执行完毕,会按照add方法栈帧的Return Address返回到main方法栈帧之前执行中断的位置继续执行,而同时add方法栈帧会出栈销毁(执行完毕没有用了)。
那么当方法嵌套调用太多,会导致JVM栈中的栈帧太多,超过了栈大小的限制,就会报错Stack Overflow。这部分在GC内容中会详细学习。
上面有介绍到一些invoke指令,例如invokeVirtual、InvokeStatic等。JVM的invoke指令总共有:
lambda表达式的动态产生的类,会用到InvokeDynamic。输出的情况,看到都是Lambda的动态产生的类的情况。看一下字节码的内容:
可以验证到,字节码的指令都是用到了InvokeDynamic。
JVM规范中还有很多没有涉及到的指令,可以在用到的时候去手动查询,重点关注操作数栈的变化以及执行的描述。