字节码文件是Java虚拟机跨平台/跨语言的基础.
Java虚拟机只与字节码文件绑定,至于字节码文件的源代码是否是Java代码编写就不是JVM考虑的问题了,这也是JVM的强大之处.
字节码的生成由前端编译器生成,前端编译器可以是多种语言的编译器,将源代码编译为符合JVM规范的class文件,交由JVM解释执行.
以Java为例,jdk中就包含了可以编译java源码的编译器,我们可以使用javac来将一个java源代码文件编译为class文件.
而通过对class文件的详细了解,我们可以看到源代码中无法表现的一些细节,编译器是如何对源代码进行优化编译的.
字节码文件是二进制文件.
字节码文件由 操作码和操作数组成、例如 bipush 20 前面的就是操作码,20就是操作数,操作数可以没有
javac就是一种前端编译器,也是我们使用最多的编译器,目前在idea里默认的编译器就是javac
javac是一种全量编译器,就是不管java源代码改了多少,都是全量编译.
Eclipse提供了一种区别于javac的编译器给,叫ECJ(Eclipse Compiler For Java),属于Eclipse的插件,他是一个增量的编译器,所以速度要比javac快,而且质量不差多少
AspectJ Compiler 可以作为idea的一个插件与javac配合使用,提高编译效率
通过几个例子的字节码查看,理解字节码的好处.
3.1 例一先来一个例子:
public class ClassTest { public static void main(String[] args) { Integer i1 = 10; int i2 = 10; System.out.println(i1 == i2); }}
结果为true,为什么呢?
我们来看一下字节码,就很清楚了:
通过第二行我们可以看到 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
字符串拼接里在从jvm查看字符串
package com.zy.study15;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
从字节码里你就可以看到多个String对象相加是怎么操作了,创建的StringBuilder对象进行append操作,最后调用toString方法
package com.zy.study15;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); }}
打印结果
解释这个结果只需要看下字节码就可以了
new Son()本质上就是调用了构造器,所以我们看下构造器的方法
0 aload_0 this对象 1 invokespecial #10
0 aload_0 1 invokespecial #10
解释下:
new Son()会先调用Son类的构造方法,具体执行过程可以看上面的Son类的init字节码从Son的init构造方法的执行过程中可以看到,在第二行调用父类Father的构造方法.这个时候再去调用Father的构造方法可以看到在打印num的时候,只给num赋了初始值0,所以第一次打印的值为0并且由于多态,Print方法被子类Son重写,所以打印的内容里是Son打印,而不是Father打印执行完父类的构造方法后,继续回到Son类的init方法可以看到打印num之前给num赋了100,所以打印num的值是100再之后给num赋了200,所以再获取Son的num的值就是200了.最后一行的输出为20,这是因为我们获取的是Father的num值,而不是Son的num的值. 3.3 总结 通过字节码可以看到一些不容易理解的代码的执行过程.
安装Binary Viewer工具,可以打开字节码文件
**Binary Viewer安装包 **https://www.aliyundrive.com/s/5HCJfAFQoxf 提取码0eo6
或者Notepad++插件 Hex-Editor
直接在插件市场安装即可(如果插件市场有问题,尝试安装最新版本notepad++)
# 将class文件反编译后的内容输出到当前目录javap -v ClassTest.class > ClassTestOut.java
idea插件市场安装 jclasslib即可
效果如下: