深入理解java字节码 - Go语言中文社区

深入理解java字节码


深入理解JVM—字节码执行引擎  

2012-03-20 16:10:17|  分类: JVM |  标签:jvm  字节码  执行引擎  class  分派  |字号 订阅

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

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 从

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

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 那

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

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 我们从上图可以看出。

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

 

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 我们知道

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

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 在栈帧中,最小的单位为变量槽

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

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

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 Slot

复用会给JVM的垃圾回收带来一定影响,如下代码:

package com.yhj.jvm.byteCode.slotFree;

/**

 * @DescribedSlot局部变量表 没有破坏GCRoot情况演示

 * @VM params :-XX:+PrintGCDetails -verbose:gc

 * @author YHJ create at 2012-2-22 下午04:37:29

 * @FileNmae com.yhj.jvm.byteCode.slotFree.SlotFreeTestCase.java

 */

public class SlotFreeTestCase {

 

    /**

     * @param args

     * @Author YHJ create at 2012-2-22 下午04:37:25

     */

    @SuppressWarnings("unused")

    public static void main(String[] args) {

       //case 1

       byte[] testCase = new byte[10*1024*1024];

       System.gc();

//    

//     //case 2

//     {

//         byte[] testCase = new byte[10*1024*1024];

//     }

//     System.gc();

//    

//     //case 3

//     {

//         byte[] testCase = new byte[10*1024*1024];

//     }

//     int a = 0;

//     System.gc();

//    

//     //case 5

//     byte[] testCase = new byte[10*1024*1024];

//     testCase=null;

//     System.gc();

    }

 

}

如上所示,当我们执行这段代码的时候并不会引发GC的回收,因为很简单,我的testCase对象还在使用中,生命周期并未结束,因此运行结果如下

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 但是我们换下面的

case2这种写法呢?

       //case 2

       {

           byte[] testCase = new byte[10*1024*1024];

       }

       System.gc();

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

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰
  我们可以看到仍然没有进行回收。

那我变通一下,再定义一个变量会怎么样的呢?

       //case 3

       {

           byte[] testCase = new byte[10*1024*1024];

       }

       int a = 0;

       System.gc();

这下我们貌似看到奇迹了

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 没错,

JVM做了回收操作,因为JVM在做下面的操作时并没有发现公用的Slot,因此该内存区域被回收。但是我们这样写代码会让很多人感到迷惑,我们应该怎样写才能更好一点让人理解的呢?

       //case 5

       byte[] testCase = new byte[10*1024*1024];

       testCase=null;

       System.gc();

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

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

package com.yhj.jvm.byteCode.localVariableInit;

/**

 * @Described:局部变量拒绝默认初始化

 * @author YHJ create at 2012-2-24 下午08:40:34

 * @FileNmae com.yhj.jvm.byteCode.localVariableInit.LocalVariableInit.java

 */

public class LocalVariableInit {

 

    /**

     * @param args

     * @Author YHJ create at 2012-2-22 下午05:12:06

     */

    @SuppressWarnings("unused")

    public static void main(String[] args) {

       int a;

       System.out.println(a);

    }

}

 

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

很多人会回答0.我们来看一下运行结果:

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 没错,就是报错了,如果你使用的是

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

我们在类的加载中提到类的静态连接过程,但是还有一部分类是需要动态连接的,其中以下是需要动态连接的对象

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

2、  通过其他荣报告期动态注入的变量(IOC)

3、  通过代码注入的对象(void setObj(Object obj))

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

前面我们提到JVM的生命周期,在以下四种情况下会引发JVM的生命周期结束

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

2、  程序正常运行结束

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

4、  由于操作系统出现错误而导致JVM进程终止

同样,在以下情况下会导致一个方法调用结束

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

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

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

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

2、  如果有返回值的话将返回值压入到上层操作数栈

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

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

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

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

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

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

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

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

2、  invokeinterface 调用接口方法,不确定调用哪一个实现类

我们可以通过javap的命令查看对应的字节码文件方法调用的方式,如下图所示

 

深入理解JVM—字节码执行引擎 - 一线天色 天宇星辰 - 一线天色 天宇星辰

 Java

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

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

package com.yhj.jvm.byteCode.staticDispatch;

/**

 * @Described:静态分配

 * @author YHJ create at 2012-2-24 下午08:20:06

 * @FileNmae com.yhj.jvm.byteCode.staticDispatch.StaticDispatch.java

 */

public class StaticDispatch {

 

    static abstract class Human{};

    static class Man extends Human{} ;

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/kingj126/article/details/84230813
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

  • 发表于 2020-03-06 22:05:54
  • 阅读 ( 1337 )
  • 分类:Go深入理解

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢