JVM深入学习(二十五)-字节码文件概述

1. 概述

字节码文件是Java虚拟机跨平台/跨语言的基础.

Java虚拟机只与字节码文件绑定,至于字节码文件的源代码是否是Java代码编写就不是JVM考虑的问题了,这也是JVM的强大之处.

字节码的生成由前端编译器生成,前端编译器可以是多种语言的编译器,将源代码编译为符合JVM规范的class文件,交由JVM解释执行.

以Java为例,jdk中就包含了可以编译java源码的编译器,我们可以使用javac来将一个java源代码文件编译为class文件.

而通过对class文件的详细了解,我们可以看到源代码中无法表现的一些细节,编译器是如何对源代码进行优化编译的.

字节码文件是二进制文件.

字节码文件由 操作码操作数组成. 例如 bipush 20 前面的就是操作码,20就是操作数,操作数可以没有

2. 前端编译器

2.1 javac

javac就是一种前端编译器,也是我们使用最多的编译器,目前在idea里默认的编译器就是javac

javac是一种全量编译器,就是不管java源代码改了多少,都是全量编译.

JVM深入学习(二十五)-字节码文件概述


2.2 ECJ

Eclipse提供了一种区别于javac的编译器给,叫ECJ(Eclipse Compiler For Java),属于Eclipse的插件,他是一个增量的编译器,所以速度要比javac快,而且质量不差多少

2.3 AJC

AspectJ Compiler 可以作为idea的一个插件与javac配合使用,提高编译效率

3. 学习查看字节码的优点

通过几个例子的字节码查看,理解字节码的好处.

3.1 例一

先来一个例子:

public class ClassTest {
    public static void main(String[] args) {
        Integer i1 = 10;
        int i2 = 10;
        System.out.println(i1 == i2);
    }
}

结果为true,为什么呢?

我们来看一下字节码,就很清楚了:

JVM深入学习(二十五)-字节码文件概述


通过第二行我们可以看到 i1的定义本质上是一个Integer.valueOf方法,我们进入该方法看一下:

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
// 进入cache方法 静态内部类
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() {}
    }

可以看到是一个IntegerCache在获取了值 (int为10 不超过low的值-128 high的值127); IntegerCache对-128到127这256个数字做了缓存,所以取到的数字10本质上就是int 10

所以输出为true

3.2 例二

字符串拼接里在从jvm查看字符串

package com.zy.study15;
/**
 * @Author: Zy
 * @Date: 2022/2/7 15:45
 * 字节码查看字符串拼接
 */
public class ClassTest {
    public static void main(String[] args) {
        // 其实是StringBuilder的拼接 生成的是一个新的String对象
        String helloWorld = new String("hello") + new String("world");
        // 字符串常量池
        String helloWorld1 = "helloworld";
        // 不相等 false
        System.out.println(helloWorld == helloWorld1);
        // 也是新创建的对象
        String helloworld2 = new String("helloworld");
        // 不相等 false
        System.out.println(helloWorld1 == helloworld2);
    }
}

字节码如下:

0 new #2 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
 7 new #4 <java/lang/String>
10 dup
11 ldc #5 <hello>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <world>
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
34 astore_1
35 ldc #10 <helloworld>
37 astore_2
38 getstatic #11 <java/lang/System.out : Ljava/io/PrintStream;>
41 aload_1
42 aload_2
43 if_acmpne 50 (+7)
46 iconst_1
47 goto 51 (+4)
50 iconst_0
51 invokevirtual #12 <java/io/PrintStream.println : (Z)V>
54 new #4 <java/lang/String>
57 dup
58 ldc #10 <helloworld>
60 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
63 astore_3
64 getstatic #11 <java/lang/System.out : Ljava/io/PrintStream;>
67 aload_2
68 aload_3
69 if_acmpne 76 (+7)
72 iconst_1
73 goto 77 (+4)
76 iconst_0
77 invokevirtual #12 <java/io/PrintStream.println : (Z)V>
80 return

 从字节码里你就可以看到多个String对象相加是怎么操作了,创建的StringBuilder对象进行append操作,最后调用toString方法

3.3 例三

package com.zy.study15;
/**
 * @Author: Zy
 * @Date: 2022/2/9 18:44
 * 父子类从字节码看
 */
public class ClassTest2 {
    static class Father{
        int num = 0;
        void print(){
            System.out.println("Father打印,num的值为"+num);
        }
        Father(){
            print();
            num = 20;
        }
    }
    static class Son extends Father{
        int num = 100;
        @Override
        void print(){
            System.out.println("Son打印,num的值为"+num);
        }
        Son(){
            print();
            num = 200;
        }
    }
    public static void main(String[] args) {
        // 多态 父类子类的构造器里都有打印,那么打印num的结果是什么,为什么
        Father father = new Son();
        System.out.println(father.num);
    }
}

打印结果

JVM深入学习(二十五)-字节码文件概述

解释这个结果只需要看下字节码就可以了

new Son()本质上就是调用了构造器,所以我们看下构造器的方法


3.3.1 Father类init字节码

0 aload_0 this对象
 1 invokespecial #10 <java/lang/Object.<init> : ()V> 父类Object的构造方法
 4 aload_0
 5 iconst_0 常量0 也就是num的默认值
 6 putfield #6 <com/zy/study15/ClassTest2$Father.num : I> 给num赋默认初始值0
 9 aload_0 
10 invokevirtual #11 <com/zy/study15/ClassTest2$Father.print : ()V> 调用Print方法,打印num
13 aload_0
14 bipush 20 构造方法中的给num赋值20,这里是取出常量20
16 putfield #6 <com/zy/study15/ClassTest2$Father.num : I> 给num赋值20
19 return


3.3.2 Son类init字节码

0 aload_0
 1 invokespecial #10 <com/zy/study15/Father.<init> : ()V> 同理,父类Father的构造方法
 4 aload_0
 5 bipush 100 取出100 
 7 putfield #6 <com/zy/study15/Son.num : I> 给num赋100
10 aload_0
11 invokevirtual #11 <com/zy/study15/Son.print : ()V> Print打印方法,打印num
14 aload_0
15 sipush 200 取出常量200
18 putfield #6 <com/zy/study15/Son.num : I> 给num赋值200
21 return

解释下:

  1. new Son()会先调用Son类的构造方法,具体执行过程可以看上面的Son类的init字节码
  2. 从Son的init构造方法的执行过程中可以看到,在第二行调用父类Father的构造方法.
  3. 这个时候再去调用Father的构造方法
  4. 可以看到在打印num的时候,只给num赋了初始值0,所以第一次打印的值为0
  5. 并且由于多态,Print方法被子类Son重写,所以打印的内容里是Son打印,而不是Father打印
  6. 执行完父类的构造方法后,继续回到Son类的init方法
  7. 可以看到打印num之前给num赋了100,所以打印num的值是100
  8. 再之后给num赋了200,所以再获取Son的num的值就是200了.
  9. 最后一行的输出为20,这是因为我们获取的是Father的num值,而不是Son的num的值.

3.3 总结

通过字节码可以看到一些不容易理解的代码的执行过程.

4. 查看字节码文件的方法

4.1 第三方软件

安装Binary Viewer工具,可以打开字节码文件

Binary Viewer安装包 https://www.aliyundrive.com/s/5HCJfAFQoxf 提取码0eo6

JVM深入学习(二十五)-字节码文件概述

或者Notepad++插件 Hex-Editor

直接在插件市场安装即可(如果插件市场有问题,尝试安装最新版本notepad++)

JVM深入学习(二十五)-字节码文件概述

JVM深入学习(二十五)-字节码文件概述


4.2 jdk自带javap

# 将class文件反编译后的内容输出到当前目录

javap -v ClassTest.class > ClassTestOut.java

JVM深入学习(二十五)-字节码文件概述

JVM深入学习(二十五)-字节码文件概述


4.3 idea插件 jclasslib

idea插件市场安装 jclasslib即可

效果如下:

JVM深入学习(二十五)-字节码文件概述

上一篇:java 多态和内部类


下一篇:Linux中总线设备驱动模型及平台设备驱动实例