Java之深刻JVM(6) - 字节码执行引擎(转)

本文为转载,来自java

 前面咱们不止一次的提到,Java是一种跨平台的语言,为何能够跨平台,由于咱们编译的结果是中间代码字节码,而不是机器码,那字节码在整个Java平台扮演着什么样的角色的呢?JDK1.2以前对应的结构图以下所示:linux

JDK1.2开始,迫于Java运行始终笔C++慢的压力,JVM的结构也慢慢发生了一些变化,JVM在某些场景下能够操做必定的硬件平台,一些核心的Java库甚至也能够操做底层的硬件平台,从而大大提高了Java的执行效率,在前面JVM内存模型和垃圾回收中也给你们演示了如何操做物理内存,下图展现了JDK1.2以后的JVM结构模型。android

C++Java在编译和运行时到底有啥不同?为啥Java就能跨平台的呢?面试

 咱们从上图能够看出。安全

C++发布的就是机器指令,而Java发布的是字节码,字节码在运行时经过JVM作一次转换生成机器指令,所以可以更好的跨平台运行。如图所示,展现了对应代码从编译到执行的一个效果图。oracle

 咱们知道JVM是基于栈执行的,每一个线程会创建一个操做栈,每一个栈又包含了若干个栈帧,每一个栈帧包含了局部变量、操做数栈、动态链接、方法的返回地址信息等。其实在咱们编译的时候,须要多大的局部变量表、操做数深度等已经肯定并写入了Code属性,所以运行时内存消耗的大小在启动时已经已知。app

在栈帧中,最小的单位为变量槽(Variable Slot),其中每一个Slot占用32个字节。在32bitJVM32位的数据类型占用1Slot64bit数据占用2Slot;在64bit中使用64bit字节填充来模拟32bit(又称补位),所以咱们能够得出结论:64bitJVM32bit的更消耗内存,可是又出32bit机器的内存上限限制,有时候牺牲一部分仍是值得的。Java的基本数据类型中,除了longdouble两种数据类型为64bit之外,booleanbytecharintfloatreference等都是32bit的数据类型。jvm

在栈帧中,局部变量表中的Slot是能够复用的,如在一个方法返回给上一个方法是就能够经过公用Slot的方法来节约内存控件,但这在必定程度省会影响垃圾回收,所以JVM不肯定这块Slot空间是否还须要复用。ide

 Slot复用会给JVM的垃圾回收带来必定影响,以下代码:函数

 1 package com.yhj.jvm.byteCode.slotFree;
 2 
 3 /**
 4 
 5  * @Described:Slot局部变量表 没有破坏GCRoot状况演示
 6 
 7  * @VM params :-XX:+PrintGCDetails -verbose:gc
 8 
 9  * @author YHJ create at 2012-2-22 下午04:37:29
10 
11  * @FileNmae com.yhj.jvm.byteCode.slotFree.SlotFreeTestCase.java
12 
13  */
14 
15 public class SlotFreeTestCase {
16 
17     /**
18 
19      * @param args
20 
21      * @Author YHJ create at 2012-2-22 下午04:37:25
22 
23      */
24 
25     @SuppressWarnings("unused")
26 
27     public static void main(String[] args) {
28 
29        //case 1
30 
31        byte[] testCase = new byte[10*1024*1024];
32 
33        System.gc();
34 
35 //     //case 2
36 
37 //     {
38 
39 //         byte[] testCase = new byte[10*1024*1024];
40 
41 //     }
42 
43 //     System.gc();
44 
45 //     //case 3
46 
47 //     {
48 
49 //         byte[] testCase = new byte[10*1024*1024];
50 
51 //     }
52 
53 //     int a = 0;
54 
55 //     System.gc();
56 
57 //     //case 5
58 
59 //     byte[] testCase = new byte[10*1024*1024];
60 
61 //     testCase=null;
62 
63 //     System.gc();
64 
65     }
66 }

如上所示,当咱们执行这段代码的时候并不会引起GC的回收,由于很简单,个人testCase对象还在使用中,生命周期并未结束,所以运行结果以下

可是咱们换下面的case2这种写法呢?

1 //case 2
2 
3        {
4 
5            byte[] testCase = new byte[10*1024*1024];
6 
7        }
8 
9        System.gc();

这种写法,testCase在大括号中生命周期已经结束了,会不会引起GC的呢?咱们来看结果:

咱们能够看到仍然没有进行回收。那我变通一下,再定义一个变量会怎么样的呢?
 1  //case 3
 2 
 3        {
 4 
 5            byte[] testCase = new byte[10*1024*1024];
 6 
 7        }
 8 
 9        int a = 0;
10 
11        System.gc();

这下咱们貌似看到奇迹了

没错,

JVM作了回收操做,由于JVM在作下面的操做时并无发现公用的Slot,所以该内存区域被回收。可是咱们这样写代码会让不少人感到迷惑,咱们应该怎样写才能更好一点让人理解的呢?

1  //case 5
2 
3        byte[] testCase = new byte[10*1024*1024];
4 
5        testCase=null;
6 
7        System.gc();

无疑,这样写才是最好的,这也是书本effective Java中强调了不少遍的写法,随手置空不用的对象。

咱们知道private int a;这么一个语句在一个类中的话他的默认值是0,那么若是是在局部变量中的呢?咱们开看这样一段代码:

 1 package com.yhj.jvm.byteCode.localVariableInit;
 2 
 3 /**
 4 
 5  * @Described:局部变量拒绝默认初始化
 6 
 7  * @author YHJ create at 2012-2-24 下午08:40:34
 8 
 9  * @FileNmae com.yhj.jvm.byteCode.localVariableInit.LocalVariableInit.java
10 
11  */
12 
13 public class LocalVariableInit {
14 
15  
16 
17     /**
18 
19      * @param args
20 
21      * @Author YHJ create at 2012-2-22 下午05:12:06
22 
23      */
24 
25     @SuppressWarnings("unused")
26 
27     public static void main(String[] args) {
28 
29        int a;
30 
31        System.out.println(a);
32 
33     }
34 
35 } 

这段代码的运营结果又是什么的呢?

不少人会回答0.咱们来看一下运行结果:

没错,就是报错了,若是你使用的是

Eclipse这种高级一点的IDE的 话,在编译阶段他就会提示你,该变量没有初始化。缘由是什么的呢?缘由就是,局部变量并无类实例变量那样的链接过程,前面咱们说过,类的加载分为加载、 链接、初始化三个阶段,其中链接氛围验证、准备、解析三个阶段,而验证是确保类加载的正确性、准备是为类的静态变量分配内存,并初始化为默认值、解析是把 类中的符号引用转换为直接引用。而外面的初始化为类的静态变量赋值为正确的值。而局部变量并无链接的阶段,所以没有赋值为默认值这一阶段,所以必须本身 初始化才能使用。

咱们在类的加载中提到类的静态链接过程,可是还有一部分类是须要动态链接的,其中如下是须要动态链接的对象

一、  实例变量(类的变量或者局部变量)

二、  经过其余荣报告期动态注入的变量(IOC)

三、  经过代码注入的对象(void setObj(Object obj))

全部的动态链接都只有准备和解析阶段,没有再次校验(校验发生在链接前类的加载阶段),其中局部变量不会再次引起准备阶段。

前面咱们提到JVM的生命周期,在如下四种状况下会引起JVM的生命周期结束

一、  执行了System.exit()方法

二、  程序正常运行结束

三、  程序在执行过程当中遇到了异常或者错误致使异常终止

四、  因为操做系统出现错误而致使JVM进程终止

一样,在如下状况下会致使一个方法调用结束

一、  执行引擎遇到了方法返回的字节码指令

二、  执行引擎在执行过程当中遇到了未在该方法内捕获的异常

这时候不少人会有一个疑问:当程序返回以后它怎么知道继续在哪里执行?这就用到了咱们JVM内存模型中提到了的PC计数器。方法退出至关于当前栈出栈,出栈后主要作了如下事情:

一、  回复上层方法的局部变量表

二、  若是有返回值的话将返回值压入到上层操做数栈

三、  调整PC计数器指向下一条指令

除了以上信息之外,栈帧中还有一些附加信息,如预留一部份内存用于实现一些特殊的功能,如调试信息,远程监控等信息。

接下来咱们要说的是方法调用,方法调用并不等于方法执行,方法调用的任务是肯定调用方法的版本(调用哪个方法),在实际过程当中有可能发生在加载期间也有可能发生在运行期。Class的编译过程并不包含相似C++的链接过程,只有在类的加载或者运行期才将对应的符号引用修正为真正的直接引用,大大的提高了Java的灵活性,可是也大大增长了Java的复杂性。

在类加载的第二阶段链接的第三阶段解析,这一部分是在编译时就肯定下来的,属于编译期可知运行期不可变。在字节码中主要包含如下两种

一、  invokestatic 主要用于调用静态方法,属于绑定类的调用

二、  invokespecial 主要用于调用私有方法,外部不可访问,绑定实例对象

还有一种是在运行时候解析的,只有在运行时才能肯定下来的,主要包含如下两方面

一、  invokevirtual 调用虚方法,不肯定调用那一个实现类

二、  invokeinterface 调用接口方法,不肯定调用哪个实现类

咱们能够经过javap的命令查看对应的字节码文件方法调用的方式,以下图所示

Java

方法在调用过程当中,把invokestaticinvokespecial定义为非虚方法的调用,非虚方法的调用都是在编译器已经肯定具体要调用哪个方法,在类的加载阶段就完成了符号引用到直接引用的转换。除了非虚方法之外,还有一种被final修饰的方法,因被final修饰之后调用没法经过其余版原本覆盖,所以被final修饰的方法也是在编译的时候就已知的废墟方法。

除了解析,Java中还有一个概念叫分派,分派是多态的最基本表现形式,可分为单分派、多分派两种;同时分派又能够分为静态分派和动态分派,所以一组合,能够有四种组合方式。其实最本质的体现就是方法的重载和重写。咱们来看一个例子

 1 package com.yhj.jvm.byteCode.staticDispatch;
 2 
 3 /**
 4 
 5  * @Described:静态分配
 6 
 7  * @author YHJ create at 2012-2-24 下午08:20:06
 8 
 9  * @FileNmae com.yhj.jvm.byteCode.staticDispatch.StaticDispatch.java
10 
11  */
12 
13 public class StaticDispatch {
14 
15  
16 
17     static abstract class Human{};
18 
19     static class Man extends Human{} ;
20 
21     static class Woman extends Human{} ;
22 
23  
24 
25     public void say(Human human) {
26 
27        System.out.println("hi,you are a good human!");
28 
29     }
30 
31     public void say(Man human) {
32 
33        System.out.println("hi,gentleman!");
34 
35     }
36 
37     public void say(Woman human) {
38 
39        System.out.println("hi,yong lady!");
40 
41     }
42 
43     /**
44 
45      * @param args
46 
47      * @Author YHJ create at 2012-2-24 下午08:20:00
48 
49      */
50 
51     public static void main(String[] args) {
52 
53        Human man = new Man();
54 
55        Human woman = new Woman();
56 
57        StaticDispatch dispatch = new StaticDispatch();
58 
59        dispatch.say(man);
60 
61        dispatch.say(woman);
62 
63     }
64 
65 }

这个例子的执行结果会是什么呢?咱们来看一下结果

和你的预期一致么?这个实际上是一个静态分派的杯具,

manwoman两个对象被转型之后,经过特征签名匹配,只能匹配到对应的父类的重载方法,所以致使最终的结构都是执行父类的代码。由于具体的类是在运行期才知道具体是什么类型,而编译器只肯定是Human这种类型的数据。

这种写法曾经在咱们项目中也发生过一次。以下代码所示

 

 

 1 package com.yhj.jvm.byteCode.staticDispatch;
 2 
 3 import java.util.ArrayList;
 4 
 5 import java.util.List;
 6 
 7 /**
 8 
 9  * @Described:蝌蚪网曾经的杯具
10 
11  * @author YHJ create at 2012-2-26 下午09:43:20
12 
13  * @FileNmae com.yhj.jvm.byteCode.staticDispatch.CothurnusInPassport.java
14 
15  */
16 
17 public class CothurnusInPassport {
18 
19     /**
20 
21      * 主函数入口
22 
23      * @param args
24 
25      * @Author YHJ create at 2012-2-26 下午09:48:02
26 
27      */
28 
29     public static void main(String[] args) {
30 
31        List<CothurnusInPassport> inPassports = new ArrayList<CothurnusInPassport>();
32 
33        inPassports.add(new CothurnusInPassport());
34 
35        String xml = XML_Util.createXML(inPassports);
36 
37        System.out.println(xml);
38 
39     }
40 
41 }
42 
43 class XML_Util{
44 
45     public static String createXML(Object obj){
46 
47        return  。。。// ... 经过反射遍历属性 生成对应的XML节点
48 
49     }
50 
51     public static String createXML(List<Object> objs){
52 
53        StringBuilder sb = new StringBuilder();
54 
55        for(Object obj : objs)
56 
57            sb.append(createXML(obj));
58 
59        return new String(sb);
60 
61     }
62 
63 }

当时咱们项目组写了以恶搞XML_Util的一个类用于生成各类XML数据,其中一个实例传入的参数是Object,一个是一个List类型的数据,如上面代码所示,个人调用结果会执行哪个的呢?结果你们已经很清楚了,他调用了createXML(Object obj)这个方法,所以生成过程当中总是报错,缘由很简单,就是由于我叼用的时候泛型 不匹配,进行了隐式的类型转换,所以没法匹配到对应的List《Object》最终调用了createXML(Object obj)这个方法。

下面咱们来看一道恶心的面试题,代码以下:

 1 package com.yhj.jvm.byteCode.polymorphic;
 2 
 3 import java.io.Serializable;
 4 
 5 /**
 6 
 7  * @Described:重载测试
 8 
 9  * @author YHJ create at 2012-2-24 下午08:41:12
10 
11  * @FileNmae com.yhj.jvm.byteCode.polymorphic.OverLoadTestCase.java
12 
13  */
14 
15 public class OverLoadTestCase {
16 
17     public static void say(Object obj){ System.out.println("Object"); }
18    
19     public static void say(char obj){ System.out.println("char"); }
20 
21     public static void say(int obj){ System.out.println("int"); }
22 
23     public static void say(long obj){ System.out.println("long"); }
24 
25     public static void say(float obj){ System.out.println("float"); }
26 
27     public static void say(double obj){ System.out.println("double"); }
28 
29     public static void say(Character obj){ System.out.println("Character"); }
30 
31     public static void say(Serializable obj){ System.out.println("Serializable"); }
32 
33     public static void say(char... obj){ System.out.println("char..."); }
34 
35     public static void main(String[] args) {
36 
37        OverLoadTestCase.say('a');
38 
39     }
40 
41 }

这样的代码会执行什么呢?这个很简单的了,是char,那若是我注释掉char这个方法,再执行呢?是int,继续注释,接下来是什么的呢?你们能够本身测试一下,你会发现这段代码有多么的恶心。

咱们接下来再看一段代码:

 1 package com.yhj.jvm.byteCode.dynamicDispatch;
 2 
 3 /**
 4 
 5  * @Described:动态分派测试
 6 
 7  * @author YHJ create at 2012-2-26 下午10:05:43
 8 
 9  * @FileNmae com.yhj.jvm.byteCode.dynamicDispatch.DynamicDispatch.java
10 
11  */
12 
13 public class DynamicDispatch {
14 
15     static abstract class Human{
16 
17        public abstract void say();
18 
19     };
20 
21     static class Man extends Human{
22 
23        @Override
24 
25        public void say(){
26 
27            System.out.println("hi,you are a good man!");
28 
29        }
30 
31     } ;
32 
33     static class Woman extends Human{
34 
35        @Override
36 
37        public void say(){
38 
39            System.out.println("hi,young lady!");
40 
41        }
42 
43     } ;
44 
45     //主函数入口
46 
47     public static void main(String[] args) {
48 
49        Human man = new Man();
50 
51        Human woman = new Woman();
52 
53        man.say();
54 
55        woman.say();
56 
57        woman = new Man();
58 
59        woman.say();
60 
61     }
62 
63 }

这段代码执行的结果会是什么的呢?这个不用说了吧?企业级的应用常常会使用这些的方法重写,这是动态分配的一个具体体现,也就是说只有运行期才知道具体执行的是哪个类,在编译期前并不知道会调用哪个类的这个方法执行。

咱们再来看一段代码,这段代码被称为“一个艰难的决定”

 1 //动态单分派静态多分派    宗量选择
 2 
 3 package com.yhj.jvm.byteCode.dynamicOneStaticMoreDispatch;
 4 
 5 /**
 6 
 7  * @Described:一个艰难的决定
 8 
 9  * @author YHJ create at 2012-2-24 下午09:23:26
10 
11  * @FileNmae com.yhj.jvm.byteCode.dynamicOneStaticMore.OneHardMind.java
12 
13  */
14 
15 public class OneHardMind {
16     static class QQ{}                //腾讯QQ
17 
18     static class _360{}             //360安全卫士
19 
20     static class QQ2011 extends QQ{} //腾讯QQ2011
21 
22     static class QQ2012 extends QQ{} //腾讯QQ2012
23 
24     //百度
25 
26     static class BaiDu{
27 
28        public static void choose(QQ qq){ System.out.println("BaiDu choose QQ"); }
29 
30        public static void choose(QQ2011 qq){ System.out.println("BaiDu choose QQ2011"); }
31 
32        public static void choose(QQ2012 qq){ System.out.println("BaiDu choose QQ2012"); }
33 
34        public static void choose(_360 _){ System.out.println("BaiDu choose 360 safe"); }
35 
36     }
37 
38     //迅雷
39 
40     static class Thunder{
41 
42        public static void choose(QQ qq){ System.out.println("Thunder choose QQ"); }
43 
44        public static void choose(QQ2011 qq){ System.out.println("Thunder choose QQ2011"); }
45 
46        public static void choose(QQ2012 qq){ System.out.println("Thunder choose QQ2012"); }
47 
48        public static void choose(_360 qq){ System.out.println("Thunder choose 360 safe"); }
49 
50     }
51 
52     //主函数入口
53 
54     @SuppressWarnings("static-access")
55 
56     public static void main(String[] args) {
57 
58        BaiDu baiDu = new BaiDu();
59 
60        Thunder thunder = new Thunder();
61 
62        QQ qq = new QQ();
63 
64        _360 _360_safe = new _360();
65 
66        baiDu.choose(qq);
67 
68        thunder.choose(_360_safe);
69 
70        qq = new QQ2011();
71 
72        baiDu.choose(qq);
73 
74        qq = new QQ2012();
75 
76        baiDu.choose(qq);
77 
78     }
79 }

这段代码的执行结果又是什么?如今能够很简单的说出对应的结果了吧!

从这个例子咱们能够看出,

Java是静态多分派动态单分派的 同理,C#3.0 C++也是静态多分配,动态单分派的 C#4.0后引入类型dynamic能够实现动态多分派,sun公司在JSR-292中提出了动态多分派的实现,规划在JDK1.7推出,可是被oracle收购后,截至目前,JDK1.7已经不发布了多个版本,但还没有实现动态多分派。至于动态多分派到底是怎么样子的?咱们能够参考Python的多分派实例

那虚拟机为何可以实现不一样的类加载不一样的方法,何时使用静态分派?何时又使用动态分派呢?咱们把上面的示例用一个图来表示,你们就很清楚了!

 

当 子类有重写父类的方法时,在系统进行解析的时候,子类没有重写的方法则将对应的符号引用解析为父类的方法的直接引用,不然解析为本身的直接引用,所以重写 永远都会指向本身的直接引用,可是重载在解析时并不知道具体的直接引用对象是哪个?因此只能解析为对应的表象类型的方法。

咱们在前面已经提到,新的Java编译器已经不会纯粹的走解释执行之路,在一些状况下还会走编译之路。以下图所示:

咱们知道,程序之因此能运行,是由于有指令集,而

JVM主要是基于栈的一个指令集,而还有一部分程序是基于寄存器的指令集,二者有什么区别的呢?

基 于栈的指令集有接入简单、硬件无关性、代码紧凑、栈上分配无需考虑物理的空间分配等优点,可是因为相同的操做须要更多的出入栈操做,所以消耗的内存更大。 而基于寄存器的指令集最大的好处就是指令少,速度快,可是操做相对繁琐。下面咱们来看一段代码,看一下一样一段代码在不一样引擎下的执行效果有啥不一样。

 1 public class Demo {
 2 
 3     public static void foo() {
 4 
 5        int a = 1;
 6 
 7        int b = 2;
 8 
 9        int c = (a + b) * 5;
10 
11     }
12 
13 }

Client/Server VM的模式下,咱们可使用javap –verbose ${ClassName}的方式来查看对应的字节码,而基于javaDalvikVM亦能够经过platforms\android-1.6\tools目录中的dx工具查看对应的字节码。具体命令为dx --dex –verbose --dump-to=packageName --dump-method=Demo.foo --verbose-dump Demo.class

基于栈的Hotspot的执行过程以下:

基于栈的

DalvikVM执行过程以下所示:

 而基于汇编语言的展现就是这样的了

附:基于JVM的逻辑运算模型以下图所示

所以执行到

JVM上的过程就是下面的形式

相关文章
相关标签/搜索