第八章 虚拟机字节码执行引擎html
一、运行时栈帧结构java
概述:git
局部变量表:github
package com.ecut.stack; /** * -verbose:gc */ public class SlotTest { public static void main(String[] args) { //placeholder的做用域被限制在花括号以内 { byte[] placeholder = new byte[64 * 1024 * 1024]; } //若是不增长这行,即没有任何对局部变量表的读写操做,placeholder本来所占用的Slot尚未被其余变量所复用,因此做为GC Roots一部分的局部变量表仍然保持着对它的关联。 int a = 0 ; System.gc(); } }
运行结果:缓存
[GC (System.gc()) 68864K->66256K(125952K), 0.0020403 secs]
[Full GC (System.gc()) 66256K->664K(125952K), 0.0095304 secs]
操做数栈:安全
动态链接:数据结构
方法返回地址:架构
附加信息:jvm
二、方法调用ide
解析调用:
静态分派:
package com.ecut.stack; public class StaticDispatch { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public static void sayHello(Human guy) { System.out.println("hello guy"); } public static void sayHello(Man guy) { System.out.println("hello gentleman"); } public static void sayHello(Woman guy) { System.out.println("hello lady"); } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); sayHello(man); sayHello(woman); } }
运行结果:
hello guy
hello guy
“Human”称为变量的静态类型,后面的“Man”称为变量的实际类型。虚拟机(准确地说是编译器)在重载时是经过参数的静态类型而不是实际类型做为断定依据的。所以,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪一个重载版本。
全部依赖静态类型来定位方法执行版本的分派动做称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,所以肯定静态分派的动做实际上不是由虚拟机来执行的。
编译器虽然能肯定出方法的重载版本,但在不少状况下这个重载版本并非“惟一的”,每每只能肯定一个“更加合适的”版本。
package com.ecut.stack; import java.io.Serializable; public class Overload { public static void sayHello(Object arg) { System.out.println("hello Object"); } public static void sayHello(int arg) { System.out.println("hello int"); } public static void sayHello(long arg) { System.out.println("hello long"); } public static void sayHello(Character arg) { System.out.println("hello Character"); } public static void sayHello(char arg) { System.out.println("hello char"); } public static void sayHello(char... arg) { System.out.println("hello char……"); } public static void sayHello(Serializable arg) { System.out.println("hello Serializable"); } public static void main(String[] args) { sayHello('a'); } }
运行结果:
hello char 这很好理解,'a'是一个char类型的数据,天然会寻找参数类型为char的重载方法,若是注释掉sayHello(char arg)方法,那输出会变为: hello int 这时发生了一次自动类型转换,'a'除了能够表明一个字符串,还能够表明数字97(字符'a'的Unicode数值为十进制数字97),所以参数类型为int的重载也是合适的。咱们继续注释掉sayHello(int arg)方法,那输出会变为: hello long 这时发生了两次自动类型转换,'a'转型为整数97以后,进一步转型为长整数97L,匹配了参数类型为long的重载。笔者在代码中没有写其余的类型如float、double等的重载,不过实际上自动转型还能继续发生屡次,按照char->int->long->float->double的顺序转型进行匹配。但不会匹配到byte和short类型的重载,由于char到byte或short的转型是不安全的。咱们继续注释掉sayHello(long arg)方法,那输出会变为: hello Character 这时发生了一次自动装箱,'a'被包装为它的封装类型java.lang.Character,因此匹配到了参数类型为Character的重载,继续注释掉sayHello(Character arg)方法,那输出会变为: hello Serializable 这个输出可能会让人感受摸不着头脑,一个字符或数字与序列化有什么关系?出现hello Serializable,是由于java.lang.Serializable是java.lang.Character类实现的一个接口,当自动装箱以后发现仍是找不到装箱类,可是找到了装箱类实现了的接口类型,因此紧接着又发生一次自动转型。char能够转型成int,可是Character是绝对不会转型为Integer的,它只能安全地转型为它实现的接口或父类。Character还实现了另一个接口java.lang.Comparable<Character>,若是同时出现两个参数分别为Serializable和Comparable<Character>的重载方法,那它们在此时的优先级是同样的。编译器没法肯定要自动转型为哪一种类型,会提示类型模糊,拒绝编译。程序必须在调用时显式地指定字面量的静态类型,如:sayHello((Comparable<Character>)'a'),才能编译经过。下面继续注释掉sayHello(Serializable arg)方法,输出会变为: hello Object 这时是char装箱后转型为父类了,若是有多个父类,那将在继承关系中从下往上开始搜索,越接近上层的优先级越低。即便方法调用传入的参数值为null时,这个规则仍然适用。咱们把sayHello(Object arg)也注释掉,输出将会变为: hello char……
解析与分派这二者之间的关系并非二选一的排他关系,它们是在不一样层次上去筛选、肯定目标方法的过程。例如,前面说过,静态方法会在类加载期就进行解析,而静态方法显然也是能够拥有重载版本的,选择重载版本的过程也是经过静态分派完成的。
动态分派:
package com.ecut.stack; public class DynamicDispatch { static abstract class Human { protected abstract void sayHello(); } static class Man extends Human { @Override protected void sayHello() { System.out.println("man say hello"); } } static class Woman extends Human { @Override protected void sayHello() { System.out.println("woman say hello"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } }
运行结果以下:
man say hello
woman say hello
woman say hello
使用javap -verbose DynamicDispatch .class命令
invokevirtual指令的运行时解析过程大体分为如下几个步骤:
因为invokevirtual指令执行的第一步就是在运行期肯定接收者的实际类型,因此两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不一样的直接引用上,这个过程就是Java语言中方法重写的本质。咱们把这种在运行期根据实际类型肯定方法执行版本的分派过程称为动态分派。
单分派与多分派:
package com.ecut.stack; public class Dispatch { static class QQ { } static class _360 { } public static class Father { public void hardChoice(QQ arg) { System.out.println("father choose qq"); } public void hardChoice(_360 arg) { System.out.println("father choose 360"); } } public static class Son extends Father { public void hardChoice(QQ arg) { System.out.println("son choose qq"); } public void hardChoice(_360 arg) { System.out.println("son choose 360"); } } public static void main(String[] args) { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); } }
运行结果以下:
father choose 360
son choose qq
咱们来看看编译阶段编译器的选择过程,也就是静态分派的过程。这时选择目标方法的依据有两点:一是静态类型是Father仍是Son,二是方法参数是QQ仍是360。此次选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符号引用。由于是根据两个宗量进行选择,因此Java语言的静态分派属于多分派类型。
再看看运行阶段虚拟机的选择,也就是动态分派的过程。在执行“son.hardChoice(new QQ())”这句代码时,更准确地说,是在执行这句代码所对应的invokevirtual指令时,因为编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的参数“QQ”究竟是“腾讯QQ”仍是“奇瑞QQ”,由于这时参数的静态类型、实际类型都对方法的选择不会构成任何影响,惟一能够影响虚拟机选择的因素只有此方法的接受者的实际类型是Father仍是Son。由于只有一个宗量做为选择依据,因此Java语言的动态分派属于单分派类型。
虚拟机动态分派的实现:
动态类型语言支持:
JDK 1.7实现了JSR-292,新加入的java.lang.invoke包。这个包的主要目的是在以前单纯依靠符号引用来肯定调用的目标方法这种方式之外,提供一种新的动态肯定目标方法的机制,称为MethodHandle。
package com.ecut.stack; import static java.lang.invoke.MethodHandles.lookup; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; public class MethodHandleTest{ static class ClassA{ public void println(String s){ System.out.println(s); } } public static void main(String[] args)throws Throwable{ Object obj=System.currentTimeMillis()%2==0?System.out:new ClassA(); /*不管obj最终是哪一个实现类,下面这句都能正确调用到println方法*/ getPrintlnMH(obj).invokeExact("MethodHandleTest"); } private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable{ /*MethodType:表明“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及之后的参数)*/ MethodType mt=MethodType.methodType(void.class,String.class); /*lookup()方法来自于MethodHandles.lookup,这句的做用是在指定类中查找符合给定的方法名称、方法类型,而且符合调用权限的方法句柄 由于这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,表明该方法的接收者,也便是this指向的对象, 这个参数之前是放在参数列表中进行传递的,而如今提供了bindTo()方法来完成这件事情*/ return lookup().findVirtual(reveiver.getClass(),"println",mt).bindTo(reveiver); } }
MethodHandle的基本用途,不管obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),均可以正确地调用到println()方法。
三、基于栈的字节码解释引擎
解释执行的过程:
执行和编译的两种选择:
源码地址:
https://github.com/SaberZheng/jvm-test
推荐博客:
https://www.cnblogs.com/wade-luffy/archive/2016/11/13.html
转载请于明显处标明出处: