1.虚拟机字节码执行引擎
1.1 运行时栈帧结构
之前我们已经了解了有这么个区域,而且已经知道有栈帧这么一个结构。那么我们就从方法执行的角度来剖析栈帧。
1.1.1 概述
栈帧也叫过程活动记录,是编译器用来进行方法调用和方法执行的一种数据结构,它是虚拟机运行时数据区域中的虚拟机栈的栈元素。栈帧中包括了局部变量表,操作数栈,动态链接和方法返回地址以及额外的一些附加信息,在编译过程中,局部变量表的大小已经确定,操作数栈深度也已经确定,因此栈帧在运行的过程中需要分配多大的内存是固定的,不受运行时影响。对于没有逃逸的对象也会在栈上分配内存,对象的大小其实在运行时也是确定的,因此即使出现了栈上内存分配,也不会导致栈帧改变大小。
一个线程中,可能调用链会很长,很多方法都同时处于执行状态。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是最有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的字节码指令仅对当前栈帧进行操作。
- 局部变量表
- 操作数栈
- 动态连接
- 方法返回地址
- 附加信息
1.1.2 方法调用
解析
分派
动态语言支持
2 局部变量表
2.1 概述
- 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量表. java编译为class文件的时候,就在方法的Code属性的max_locals数据项中定义了该方法所需要分配的局部变量表的最大容量.
- 局部变量表的容量以变量槽(Variable Slot)为最小单位.
2.2 局部变量
- 全局变量没有赋初始值,没有问题
- 局部变量如果没有赋初始值,编译就会报错
//全局变量没有赋初始值,没有问题
//局部变量如果没有赋初始值,则会报错
public class VDemo {
private int a;
private int b;
public int add() {
return a + b;
}
public int add2() {
int a;
int b;
// return a+b; //此时提示错误,没有附初始值
return 0;
}
}
2.3 Slot
Slot:固定大小,一个slot为32字节,如果不够使用(如double等),则为用两个slot(64字节).
byte boolean short char int float double long reference returnAdress
不会出现线程安全问题,因为是线程独占的.
Slot复用
当一个变量的pc寄存器的值大于Slot的作用域的时候,Slot是可以复用的
public class GCDemo {
public static void main(String[] args) {
byte [] buff = new byte[60 * 1024 * 1024];
System.gc();//内存没有回收,因为slot会复用
}
}
//执行前VM -options中加上 -verbose:gc
//执行结果如下:
[GC (System.gc()) 66601K->62451K(247296K), 0.0229185 secs]
[Full GC (System.gc()) 62451K->62335K(247296K), 0.0131974 secs]
public class GCDemo2 {
public static void main(String[] args) {
{
byte[] buff = new byte[60 * 1024 * 1024];
}
//回收超出了buff的作用域
System.gc();//同样内存没有回收,因为slot没有在其它点使用,如果使用了就会被回收.看GCDemo3
}
}
//执行前VM -options中加上 -verbose:gc
//执行结果如下:
[GC (System.gc()) 66601K->62451K(247296K), 0.0014292 secs]
[Full GC (System.gc()) 62451K->62335K(247296K), 0.0063652 secs]
public class GCDemo3 {
public static void main(String[] args) {
{
byte[] buff = new byte[60 * 1024 * 1024];
}
int a = 10; //对slot读写了.所以会回收.
System.gc();//内存回收
}
}
//执行前VM -options中加上 -verbose:gc
//执行结果如下:
[GC (System.gc()) 66601K->62419K(247296K), 0.0141469 secs]
[Full GC (System.gc()) 62419K->895K(247296K), 0.0063767 secs]
观察以上3个GCDemo中最后一个GCDemo3的Full GC 62419K->895K,回收了 .
- buff跳出作用域(局部变量表中的对象使用完了)后不会立刻回收,而是只有新的使用才会回收复用.
就像硬盘的数据是否会覆盖一样,删除后只是标记删除,再有数据写入才会覆盖. - 2.4 线程不安全的场景 多线程 要有共享资源 对共享资源进行非原子性操作
3 操作数栈
3.1 概述
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。
操作数栈也常被称为操作栈。和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
3.2举例
public class StackNumDemo {
public int add(int a,int b){
return a+b;
}
}
虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。
- 虽然栈和栈是线程独立的,栈和栈之间会有数据传递,所以虚拟机是线上会有栈的通信.
4 动态连接
- 动态连接
每个栈帧都包含一个指向运行时常量池中该栈所属方法的引用,持有这个引用是为了支持该方法调用过程中的动态连接 (Dynamic Linking). Class文件中的常量池中存有大量的符号,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数. -
- 这种符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用,这种转化成为静态解析.
-
- 另一部分将在每一次运行期间转化为直接引用,这部分成为动态连接.
详见:7 和 8
5 方法返回地址和附加信息
5.1 方法返回地址
- 正常完成出口 执行引擎遇到任意一个方法(return)返回的字节码指令,将返回值传递个上层的方法调用者. 是否有返回值和返回值的类型根据遇到何种方法返回指令来决定.方法调用时通过一个指向方法的指针指向方法的地址,方法返回时将回归到调用处,那个地方是返回地址。
- 异常完成出口 代码中使用athrow字节码产生的,异常处理器表来处理异常返回地址.
5.2 附加信息
虚拟机规范中允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中。这部分信息完全取决于虚拟机的实现。
6 方法调用
6.1 解析调用
6.1.1 概述
- 解析
所有的方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的. 话句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用成为解析.
即:方法调用并不等同于方法的执行,方法调用阶段的唯一任务就是确定被调用方法的版本。
-
符合""编译期可知,运行期不变"要求的方法,主要包括静态方法和私有方法.
前者与类型直接关联,后者在外部不可被访问,这两种各自特性决定了他们不可通过继承或别的方式重新其它版本,因为他们适合在类加载阶段解析. -
方法调用的字节码指令有:
-
- Invokestatic 调用静态方法
-
- Invokespecial 调用实例构造器方法、私有方法、和父类方法。
-
- Invokevirtual 调用所有的虚方法。
-
- Invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象
-
- invokedynamic 现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。
前面4条指令是固化在Java虚拟机内部的,而invokedynamic是有用户所设定的引导方法决定的。
-
只要能被Invokestatic和Invokespecial指令调用的方法,都可以在解析阶段中确定唯一调用版本。 符合条件的有:
-
- 静态方法
-
- 私有方法
-
- 构造器方法
-
- final修饰的方法
-
java特性: 封装,继承,多态
6.1.2 解读
public class MethodStatic0 {
public static void sayHello(){
System.out.println("Hello");
}
public static void main(String[] args) {
MethodStatic0.sayHello();
}
}
public static void sayHello();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljav
a/io/PrintStream;
3: ldc #3 // String Hello
5: invokevirtual #4 // Method java/io/PrintStream.prin
tln:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #5 // Method sayHello:()V
3: return
LineNumberTable:
line 9: 0
line 10: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
}
SourceFile: "MethodStatic0.java"
//调用类中的MethodStatic0.sayHello静态方法,转化为字节码的时候直接指明调用的方法如下(Method sayHello:()V):
0: invokestatic #5 // Method sayHello:()V
6.2 方法调用-静态分派调用
6.2.1 概述
- 静态分派调用主要针对方法重载
- 静态分派调用主要针对方法重写
静态分派是在编译的时候就确定的.
- 只要能被Invokestatic和Invokespecial指令调用的方法,都可以在解析阶段中确定唯一调用版本。 符合条件的有:
-
- 静态方法
-
- 私有方法
-
- 构造器方法
-
- final修饰的方法
6.2.2 方法静态分派示例(-静态类)
public class MethodStatic1 {
static class Parent{
}
static class Child1 extends Parent{}
static class Child2 extends Parent{}
public void sayHello(Child1 c){
System.out.println("Child1 is call");
}
public void sayHello(Child2 c){
System.out.println("Child2 is call");
}
public void sayHello(Parent c){
System.out.println("Parent is call");
}
public static void main(String[] args) {
Parent p1 = new Child1();
Parent p2 = new Child2();
MethodStatic1 m = new MethodStatic1();
m.sayHello(p1);//Parent is call
m.sayHello(p2);//Parent is call
Parent p = new Child1();
p = new Child2(); //实际类型发生改变,但是静态方法的类型也没改变.
m.sayHello(p);//Parent is call
//静态类型强转类型发生变化
m.sayHello((Child2)p);//Child2 is call,强转之后为转后的类型.
}
}
//指定结果
Parent is call
Parent is call
Parent is call
Child2 is call
//静态方法的类型是编译的时候确定的,所以反映的是编译时候的类型.
javap -verbose 以上文件class如下:
26: invokevirtual #13 // Method sayHello:(Lcourse/jvmstu
/bytecodeexecutionengine/MethodStatic1$Parent;)V
29: aload_3
30: aload_2
31: invokevirtual #13 // Method sayHello:(Lcourse/jvmstu
/bytecodeexecutionengine/MethodStatic1$Parent;)V
34: new #7 // class course/jvmstu/bytecodeexe
cutionengine/MethodStatic1$Child1
37: dup
38: invokespecial #8 // Method course/jvmstu/bytecodeex
ecutionengine/MethodStatic1$Child1."<init>":()V
41: astore 4
43: new #9 // class course/jvmstu/bytecodeexe
cutionengine/MethodStatic1$Child2
46: dup
47: invokespecial #10 // Method course/jvmstu/bytecodeex
ecutionengine/MethodStatic1$Child2."<init>":()V
50: astore 4
52: aload_3
53: aload 4
55: invokevirtual #13 // Method sayHello:(Lcourse/jvmstu
/bytecodeexecutionengine/MethodStatic1$Parent;)V
58: aload_3
59: aload 4
61: checkcast #9 // class course/jvmstu/bytecodeexe
cutionengine/MethodStatic1$Child2
64: invokevirtual #14 // Method sayHello:(Lcourse/jvmstu
/bytecodeexecutionengine/MethodStatic1$Child2;)V
67: return
通过观察以上字节码会发现静态方法的类型是编译的时候确定的,所以反映的是编译时候的类型.
-
静态类型(Static Type) 也叫外观类型(Apparent Type): 上例中的Parent是静态类型.静态类型在编译期可知.
-
实际类型(Actual Type),上例中的Child1和Child2是实际类型. 实际类型在运行期可知.编译期间不知道实际类型是什么.
6.2.3 重载方法匹配优先级示例
- 静态分派发生在编译阶段,不是虚拟机决定的,编译器能确定出方法的重载版本,但是很多情况下这个重载版本并不是"唯一的",往往只能确定一个"更加适合的"版本.下例为演示.
public class MethodStatic2 {
public void sayHello(short a) {
System.out.println("short");
}
public void sayHello(int a) {
System.out.println("int");
}
public void sayHello(long c) {
System.out.println("long");
}
public void sayHello(char c) {
System.out.println("char");
}
public void sayHello(Character c) {
System.out.println("Character");
}
public void sayHello(Object c) {
System.out.println("Object");
}
public void sayHello(char... c) {
System.out.println("char...");
}
public void sayHello(Serializable c) {
System.out.println("Serializable...");
}
public static void main(String[] args) {
new MethodStatic2().sayHello('c');
//当不知道调用那个方法的时候会调用最匹配的
//首先待用char,然后注释掉char方法,会调用int(c还代表unicode数值十进制97),注掉int会调用long,以此类推.
//和Char最匹配的顺序:char>>int>long>>Charter>Serializable>Object>>char...
}
}
7 方法调用-动态分派调用
7.1 概述
- 静态分派调用主要针对方法重载
- 静态分派调用主要针对方法重写
动态分派(invokevirtual)的过程:
- 1.找到操作数栈顶的第一个元素所指向的对象的实际类型
- 2.1 如果在实际类型中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,2.1.1 如果通过则返回这个方法的直接引用,查找过程结束;2.1.2 如果不通过,抛出异常. 2.2 如果没有找到方法则往下3.
- 3.按照继承关系从下往上依此对实际类型的各父类进行搜索与验证
- 4.如果始终没有找到,则抛出AbstractMethodError
7.2 举例
public class MethodDynamic {
static class Parent{
public void sayHello(){
System.out.println("come to Parent");
}
}
static class Child1 extends Parent{
@Override
public void sayHello() {
System.out.println("come to Child1");
}
}
static class Child11 extends Child1{
@Override
public void sayHello() {
System.out.println("come to Child11");
}
}
static class Child2 extends Parent{
@Override
public void sayHello() {
System.out.println("come to Child2");
}
}
public static void main(String[] args) {
Parent p1 = new Child1();
Parent p2 = new Child2();
p1.sayHello();//come to Child1
p2.sayHello();//come to Child2
Parent p11 = new Child11();
p11.sayHello();//come to Child11
}
}
//执行结果
come to Child1
come to Child2
come to Child11
javap -verbose结果如下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // class course/jvmstu/bytecodeexe
cutionengine/MethodDynamic$Child1
3: dup
4: invokespecial #3 // Method course/jvmstu/bytecodeex
ecutionengine/MethodDynamic$Child1."<init>":()V
7: astore_1
8: new #4 // class course/jvmstu/bytecodeexe
cutionengine/MethodDynamic$Child2
11: dup
12: invokespecial #5 // Method course/jvmstu/bytecodeex
ecutionengine/MethodDynamic$Child2."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method course/jvmstu/bytecodeex
ecutionengine/MethodDynamic$Parent.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method course/jvmstu/bytecodeex
ecutionengine/MethodDynamic$Parent.sayHello:()V
24: new #7 // class course/jvmstu/bytecodeexe
cutionengine/MethodDynamic$Child11
27: dup
28: invokespecial #8 // Method course/jvmstu/bytecodeex
ecutionengine/MethodDynamic$Child11."<init>":()V
31: astore_3
32: aload_3
33: invokevirtual #6 // Method course/jvmstu/bytecodeex
ecutionengine/MethodDynamic$Parent.sayHello:()V
36: return
LineNumberTable:
line 30: 0
line 31: 8
line 33: 16
line 34: 20
line 37: 24
line 38: 32
line 40: 36
LocalVariableTable:
Start Length Slot Name Signature
0 37 0 args [Ljava/lang/String;
8 29 1 p1 Lcourse/jvmstu/bytecodeexecutionengine/Metho
dDynamic$Parent;
16 21 2 p2 Lcourse/jvmstu/bytecodeexecutionengine/Metho
dDynamic$Parent;
32 5 3 p11 Lcourse/jvmstu/bytecodeexecutionengine/Metho
dDynamic$Parent;
}
SourceFile: "MethodDynamic.java"
8 动态类型语言支持
8.1 概述
-
静态类型的语言在非运行阶段,变量的类型是可以确定的,也就是说变量是有类型的(int i = 0;)
-
动态类型语言在非运行阶段,变量的类型是无法确定的,也就是变量是没有类型的,但是值是有类型的,也就是运行期间可以确定变量的值的类型(var a = 20;)
-
动态语言:javascript,scale,groovy
8.2 举例
java执行javascript脚本
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class ClassDynamic {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine se= sem.getEngineByName("JavaScript");
Object obj = se.eval("function add(a,b) {return a+b} add(2,3)");
System.out.println(obj);//5.0
}
}
//执行结果
5.0