java静态分配和动态分配

来源:互联网 发布:淘宝店铺导航怎么删除 编辑:程序博客网 时间:2024/05/17 17:44

1.方法调用


先来说说java方法的调用,方法的调用不等于方法执行,方法调用阶段唯一的任务是确定被调用方法的版本(即调用哪个方法,不是唯一的,确定一个“更加合适”的版本),不涉及方法内部的具体运行过程。
我们都是知道java文件都需要编译成class文件,而一切方法调用在class文件里存储的都是符号引用,而不是方法的实际运行时内存布局的入口地址(相当于直接引用)。在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析成立的前提是:方法的程序真正运行之前就有一个可确认的调用版本,并且这个方法的调用版本在运行期是不可变的。换句话说,调用目标在程序代码写好、编辑器进行编译时就必须确认下来,这类方法调用的调用称为解析。
在Java虚拟机里提供了5条调用方法字节码指令,分别如下。
    invokestatic:调用静态方法
    invokespeciak: 调用实例构造器<init>方法、私用方法和父类方法
    invokevirtual: 调用所有的虚方法
    invokeinterface:调用接口时,会在运行再确定一个实现接口的对象
    invokedynamic:现在运行时动态解析出调用点限定符引用的方法,再执行方法
只有被invokestatic和invokespecial指令调用的方法,可以在解析阶段中确定调用的版本,符合这个条件的静态方法、私有方法、实例构造器、父类方法。它们在类加载的解析时候就会把符号引用解析为直接引用。这些方法被称为非虚方法。解析调用一定是一个静态的过程,在编译期间就完全确定,而分配调用可能是静态的也可能是动态的

2.分派


Java是一门面向对象的编程语言,因为Java具备面向对象的3个基本特征:封装、继承、多态。来看看虚拟机如何通过分派确定“重写”和”重载“方法的目标方法。

来看一个静态分配的例子

package com.jvm;/** * 静态分派 * @author renhj * */public class StaticDispatch {static class Human {     } static class Man extends Human {     } static class Women extends Human {     }public void sayHello(Human guy) {        System.out.println("hello, guy!");    }         public void sayHello(Man guy) {        System.out.println("hello, man!");    }         public void sayHello(Women guy) {        System.out.println("hello, women!");    }             public static void main(String[] args){            Human man = new Man();         Human women = new Women();                 StaticDispatch sd = new StaticDispatch();        sd.sayHello(man);           sd.sayHello(women);     } }


输出结果:



这个答案是你心目中的答案吗?

Human man = new Man(); 我们把上面代码中的“Human”称为变量的静态类型,后面的“Men”称为变量的实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期间可知的;而实际类型变化的结果在运行期间才可确定,编译器在编译程序时并不知道一个对象的实际类型是什么。
再回到上面例子代码中,mian()中两次调用sayHello()方法,在方法接受者已经确定是对象“src”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。编译器在重载时通过参数的静态类型而不是实际类型作为判断依据的,因此在编译阶段Java编译器根据参数的静态类型决定使用哪个重载版本。


我们使用javap命令输出这个类的字节码,输出结果字节码如下。




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

“更加合适”版本例子

package com.jvm;/** * 重载方法屁匹配优先级 * @author renhj * */public class Verload {private static void sayHello(char arg){System.out.println("hello char");}private static void sayHello(Object arg){System.out.println("hello Object");}private static void sayHello(int arg){System.out.println("hello int");}private static void sayHello(long arg){System.out.println("hello long");}public static void main(String[] args) {sayHello('c');}}
上面代码运行后,正常回输出:hello char,如果注释掉sayHello(char arg)方法,那输出就会变成:hello int。

3.动态分配


我们接下来看一下动态分配的过程,它和多态性的另外一个重要体现--重写(Override)有着密切的关系,先看例子。


package com.jvm;/** * 动态分派 * @author renhj * */public class DynamicDispatch {static abstract class Human {    protected abstract void sayHello();}static class Man extends Human {    @Override    protected void sayHello() {        System.out.println("hello man!");    }     } static class Women extends Human {     @Override    protected void sayHello() {        System.out.println("hello women!");    }     }             public static void main(String[] args){        Human man = new Man();        Human women = new Women();                 man.sayHello();        women.sayHello();                man = new Women();        man.sayHello();     } }
运行结果:

hello man!
hello women!
hello women!
这个结果相信不会出乎任何人的意料,那Java虚拟机是如何根据实际类型来分配方法执行版本的呢?我们使用javap命令输出这个类的字节码,尝试从中寻找答案,输出结果字节码如下。




0~15主要是建立man和woman的存储空间、调用Man和Woman类型的实例构造器,并将两个实例存放在第一个和第二个局部变量表Slot之中。接下来的16~21句是关键部分,16、20两句分别是把刚刚穿件的两个对象的引用压到栈顶,17、21两句是方法调用指令,这两条调用指令从字节角度来看,无论指令(invokevirtual)还是参数完全一样,但是这两条指令最终执行的目标方法并不相同,原因需要从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致如下几个步骤:

1). 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C.

2). 如果在类型C中找到与常量池中描述符和简单名称都相符的方法,则进行访问权限的校验,如果校验不通过,则返回java.lang.IllegaAccessError异常,校验通过则直接返回方法的直接引用,查找过程结束。

3). 否则,按照继承关系从下往上一次对C的各个父类进行第二步骤的搜索和验证过程。

4). 如果始终还是没有找到合适的方法直接引用,则抛出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步是在运行时确定接收者的实际类型,所以两次中的invokevirtual指令把常量池中的类方法符号引用解析到不同的直接引用上,这个就是java语言中方法重写的本质,我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。


4.虚拟机动态分派的实现


前面介绍的分派过程,作为对虚拟机概念模型的解析基本上已经足够了,它已经解决了虚拟机在分派中“对做什么”问题,但是虚拟机“具体是如何做到的”,可能各种虚拟机的实现都会有些差异。
由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会直接真正进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Vritual Method Table),使用虚方法表索引来代替元数据查找以提高性能。我们先看看一个虚方法表结构示例,如下图。



虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口时一致的,都指向父类的实现入口,如果之类重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。图中Son重写了来之Father的全部方法,因此Son所以的方法表没有指向父类Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以它们的方法表中所有的从Object继承来的方法都指向了Object的数据类型。
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始化后,虚拟机会把该类的方法表也初始化完毕。方法表示分派调用的“稳定优化”手段,虚拟机除了使用方法表外,在条件允许的情况下,还会使用内联缓存(Inine Cache)和基于“类型继承关系分析”技术的守护内联(Guarded Inlining)。


2 0
原创粉丝点击