Java字节码

172 阅读7分钟

1. 字节码文件

源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM指令,而不像CC++经由编译器直接生成机器码。

2. 字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码以及跟随其后的零至多个代表此操作所需参数的操作数所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。

3. 包装类缓存对象

baozhuanglei.gif

    @Test
    public void test7() {
        Integer i1 = 10;
        Integer i2 = 10;
        System.out.println(i1 == i2);// true

        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4);// false

        Boolean b1 = true;
        Boolean b2 = true;
        System.out.println(b1 == b2);// true
        
        Integer x = 128;
        int y = 128;
        System.out.println(x == y);//true
    }

对于Integer包装类值i1i2i3i4查看字节码可以看到都调用了valueOf方法,查看valueOf方法源码可知,其所允许的缓存值为-128~127,因此i1i2指向同一个地址,i1==i2结果为truei3i4都是经new实例化出来的,i3==i4结果为falseBoolean类同样使用了valueOf方法,此方法返回的truefalse是一个常量,所以b1 == b2结果为true 。包装类型x和基本类型y比较,查看字节码可知执行了intValue方法,进行了拆箱,两个值相等,故而输出trueIntegervalueOf方法及其调用值源码如下:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

BooleanvalueOf方法及其调用值源码如下:

public static Boolean valueOf(String s) {
    return parseBoolean(s) ? TRUE : FALSE;
}

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

4. 字节码文件结构组成

  • 魔数

Magic Number(魔数):字节码文件的标志。每个字节码文件开头的4个字节的无符号整数称为魔数; 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的字节码文件。即:魔数是字节码文件的标识符;魔数值固定为0xCAFEBABE,不会更改;使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

  • Class文件版本

字节码文件版本号,占用4个字节。其中第5和第6个字节代表编译的副版本号minor_version,第7和第8个字节代表编译的主版本号major_version。不同版本的Java编译器编译的Class文件对应的版本是不一样的。高版本的Java虚拟机可以执行由低版本编译器生成的字节码文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的字节码文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。在实际开发中,需注意生产环境和开发环境的JDK版本是否一致。

  • 常量池

常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用;常量池是Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一;常量池表中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  • 访问标识(或标志)

访问标记使用两个字节表示,用于识别类或者接口层次的访问信息,比如Class是类还是接口、是否定义为public类型、是否定义为abstract类型、如果是类是否声明为fianl等。

  • 类索引,父类索引,接口索引集合

指定该类的类别、父类类别以及实现的接口。其中类索引确定这个类的全限定名,父类索引用于确定这个类的父类全限定名。

  • 字段表集合

用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。(local variables)字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(publicprivateprotected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。

  • 方法表集合

指向常量池索引集合,它完整描述了每个方法的签名。

  • 属性表集合

class文件所携带的辅助信息

5. 字节码指令

  • 加载和存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。

  • 算术指令

用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈。

  • 类型转换指令

将两种不同的数值类型进行相互转换。

  • 对象的创建与访问指令

指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。

  • 方法调用与返回指令
  • 操作数栈管理指令

JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。

  • 控制转移指令

程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为 比较指令、条件跳转指令、比较条件跳转指令、多条件分支跳转指令、无条件跳转指令等。

  • 异常处理指令
  • 同步控制指令

方法级的同步、方法内指定指令序列的同步

6. Java虚拟机数据类型

Java虚拟机通过数据类型来执行计算,数据类型可分为基本数据类型和引用数据类型,基本数据类型的变量持有原始值,引用类型的变量持有引用值。Java语言中的所有基本数据类型也是Java虚拟机中的基本数据类型,其中Java虚拟机中把boolean值看成基本数据类型,由于指令集对boolean只有很有限的支持,当编译器把Java源代码编译为字节码时,会用intbyte表示boolean。在Java虚拟机中,false即整数0,true即所有非零整数。boolean数组是当byte数组来访问的。Java虚拟机还有一个只在内部使用的基本类型:returnAddressJava程序员不能使用这个类型,这个基本类型被用来实现Java程序中的finally子句。returnAddress类型不是简单意义上的数值,不属于任何一种基本类型,并且它的值是不能被运行中的程序所修改的。Java虚拟机的引用类型被统称为“引用”,有三种引用类型:类类型、接口类型、以及数组类型,它们的值都是对动态创建对象的引用。类类型的值是对类实例的引用;数组类型的值是对数组对象的引用,在Java虚拟机中,数组是个真正的对象;而接口类型的值,则是对实现了该接口的某个类实例的引用。还有一种特殊的引用值是null,它表示该引用变量没有引用任何对象。

  • 基本类型为何不放在堆中

堆比栈要大,但是栈比堆的运算速度要快。将复杂数据类型放在堆中的目的是为了不影响栈的效率,而是通过引用的方式去堆中查找。(八大基本类型的大小创建时候已经确立大小。三大引用类型创建时候无法确定大小)简单数据类型比较稳定,并且它只占据很小的内存,将它放在空间小、运算速度快的栈中,能够提高效率。