虚拟机字节码执行引擎

来源:互联网 发布:蓝科cms下载 编辑:程序博客网 时间:2024/06/05 20:08

运行时栈帧结构

栈帧是虚拟机栈当中的一种栈元素,用于支持虚拟机进行方法调用和方法执行。包括局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息

局部变量表

这是一组变量值的存储空间,用于存放方法参数方法内部定义的局部变量,以变量槽(slot)为最小单位。

为了节省栈帧空间,slot是可以重用的,方法中定义的变量,其作用于不一定会覆盖整个方法体,如果当前字节码PC计数器的值超出了某个变量的作用域,那么这个变量对应的slot就可以交给其他变量使用。

但是这个slot复用会对GC产生一定的影响:

public static void main(String[] args){    {        byte[] placeholder = new byte[64*1024*1024];    }    System.gc();}

对于上述代码这样的情况,我们使用了花括号控制了placeholder的作用域,然后再placeholder的作用域外进行System.gc(),这看起来没有什么问题,但是实际上这64M并没有被GC,这就和局部变量表slot复用有关。

我们对代码稍微进行一下代码:

public static void main(String[] args){    {        byte[] placeholder = new byte[64*1024*1024];    }    int a = 0;    System.gc();}

这一次GC触发了。

事实上placeholder是否被回收的根本原因在于局部变量表中的Slot是否还存在关于placeholder数组对象的引用

在第一段代码中,代码虽然离开了placeholder的作用域,但是之后没有涉及到任何对局部变量表的读写操作,因此placeholder原本占用的slot还没有被其他变量复用。在这种情况下就需要把其设置为null值(用来代替int a = 0),使得其对应slot清空。因此对于不使用的对象应该手动赋值为null,可以保证GC。

但是这不意味着需要把赋null值作为一种普遍的编码规范,因为以恰当的变量作用域来控制变量的回收时间才是最优雅的解决办法,更重要的是,赋null值的操作在经过JIT编译优化之后就会被擦除掉,这时设置变量为null值是没有意义的。

值得一提的是,初始化阶段没有为类变量赋值也没关系,类变量有一个零值,但是局部变量不一样,局部变量如果没有被赋初始值是不能使用的,会在编译期间就检查到并且做出提示。


分派

Java是一门面向对象的程序语言,因为Java具有面向对象3个特征:继承、封装、多态。分派调用过程会揭示多态性特征一些最基本的体现,如Overload和Override方法在JVM中是如何实现的。

1、静态分派

首先看一段重写代码:

public class StaticDispatch{    static abstract class Human{    }    static class Man extends Human{    }    static class Woman extends Human{    }    public void sayHello(Human guy){        System.out.println("I'm a guy");    }    public void sayHello(Man guy){        System.out.println("I'm a man");    }    public void sayHello(Woman guy){        System.out.println("I'm a woman");    }    public static void main(String[] args){        Human man = new Man();        Human woman = new Woman();        StaticDispatach sr = new StaticDispatch();        sr.sayHello(man);        sr.sayHello(woman);    }}
我觉得应该几乎所有敢自称学过Java的人都可以说出执行结果:

I'm a guy

I'm a guy

但是现在要多问一个问题,为什么是这样?为什么选择类型参数为Human的重载?

对于代码Human man = new Man();,"Human"称为是变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),而"Man"是变量的实际类型(Actual Type)

静态类型和实际类型在程序中都可以发生一些变化,区别是:

a、静态类型的变化仅仅在使用的时候发生,变量本身的静态类型不会改变,而且最终的静态类型在编译期是可知的

b、实际类型变化的结果在运行期才可确定

//实际类型变化

Human man = new Man();

man = new Woman();

//静态类型变化

sr.sayHello((Man)man);

sr.sayHello((Woman)man);

JVM在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。并且静态类型是编译期可知的,因此在编译阶段javac编译器会根据参数的静态类型决定使用哪个重载版本

先来看一下这样的一段代码:

class Son extends Parent{    public int a = 0;}class Parent{    public int a = 1;}public class Test {    public static void main(String[] args) {        Parent parent = new Son();        System.out.println(son.a);    }} 
这段代码的输出结果是1。

也就说明一个对象能访问到的域实际上是和它的静态类型相关的。


1、静态分配

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载,编译器虽然能确定出方法的重载版本,但是很多情况下这个重载版本不是唯一的,往往只能选出一个更加合适的版本,产生这种模糊结论的原因在于字面量不需要定义,所以没有显式的静态类型,只能通过语言上的规则去理解和推断。

看一个例子:

public static void main(String[] args) {        Run(1);    }    static void Run(int i){        System.out.println("int");    }    static void Run(long i){        System.out.println("long");    }
这个运行结果是int,但是我们会发现把int型的重载函数删除了也能运行,运行结果变为了long,因为静态类型不确定。然而我们如果改一下,改为run(long(1))就一定会执行long型的重载函数,而且如果注释了这个重载函数之后就编译不过去了,因为run(long(1))中静态类型确定了。
2、动态分配

运行期间根据实际类型确定方法执行版本的分派过程称为动态分派。

动态分配和重写有着很密切的关联。还是先来看代码:

public class DynamicDispatch{    static abstract class Human{        protected abstract void sayHello();    }    static class Man extends Human{        @Override        protected void sayHello(){            System.out.println("Man says hello");        }    }    static class Woman extends Human{        @Override        protected void sayHello(){            System.out.println("Woman says hello");        }    }    public static void main(String[] args){        Human man = new Man();        Human woman = new Woman();        woman.sayHello();        man = new Woman();        man.sayHello();    }}
运行结果显而易见:

Man says hello

Woman says hello

Woman says hello

然而我们还是要思考这是为什么?

关键在于obj.sayHello()这个地方的运行结果不一样,也就是最终执行的方法不相同,原因就需要从invokevirtual指令的多态查找过程开始说起,invokevirtual的过程实际上就是重写的本质,过程如下:

a、找到操作数栈顶的第一个元素所指向的对象的实际类型c。

b、如果在类型c当中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError。

c、否则,按照继承关系从上到下依次对c的各个父类进行第二步的搜索和验证过程。

d、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

3、单分派和多分派

单分派是根据一个宗量对目标方法进行选择,而多分派是根据多个宗量对目标方法进行选择。

Java是一门静态多分派、动态单分派的语言。


MethodHandle和Reflection机制都可以实现动态类型加载,但是他们之间存在一些区别

MethodHandle和Reflection机制的区别

1、两者都是模拟方法的调用,但是Reflection是在模拟Java代码层面的方法调用,而MethodHandle模拟的是字节码层面的方法调用。

2、Reflection是重量级的,而MethodHandle是轻量级的。

3、由于MethodHandle是对字节码方法指令调用的结果,一次你JVM在这方面做的优化使用MethodHandle方法也可以去支持,但是反射不可以。