在使用字节码文件分析 i = i++ 之前,我们先来看一些必要的前置知识,如果你已经懂了,可以直接略过
一个简单概念
以前我们经常说,Java 是跨平台的语言,但是 Java 为什么跨平台呢?其实是 JVM 的功劳,JVM其实是一种规范,HotSpot、J9、Taobao VM、Zing 等等都是它的具体实现。同时,我们也可以将 Java 虚拟机理解为执行在 OS 的一个软件,理论上也是酱紫的。Java 文件被编译成为 class 文件,然后这个 class 文件由 JVM 来进行解析执行
明白了这一点之后再来分析,既然 JVM 才是跨平台的关键,而 JVM 是用来解析执行 class 文件的,那么就可以推测出所有能编译为 class 文件的语言都是可以在 JVM 上解析执行,做到跨平台
实际上也是如此的,目前已经有巨多的语言做到了,再也不局限于 Java!比如 scala、kotlin、groovy、clojure、jython(我没有写错)等等
从跨平台的语言到跨语言的平台,你了解了么
字节码文件的结构
没有比下面这个更简单的 Java 小程序了
package com.bl.classloader;
/**
* @Author BarryLee
*/
public class ByteCode1 {
}
我们尝试着使用 sublime 打开它生成的字节码文件,下面是前面的三行的十六进制
cafe babe 0000 0034 0010 0a00 0300 0d07
000e 0700 0f01 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
然后再一个个看,能和上边对应起来的,有兴趣可以看看 JVMS 这个官方文档
magic 我们一般把他叫做魔数,其实这四字节它是标识着这个文件是什么文件,比方说这里就是 CAFE BABE,就表明了人家是一个 class 文件,想想看,正好跟 Java 的 logo 相呼应,很好记minor 小版本号
major version 大版本号,这里十六进制的 34 也就是对应着十进制的 52,也就是 Java8
constant pool_count 常量池里有多少个常量
constant pool 常量池里都有些啥,划重点,等等要烤的
access flags 这个类的标识信息,比如 private、default、protected、public,是 class 还是 interface
this_class 当前类
super_class 还要讲嘛,父类
interface_count 接口的数量
interfaces 具体的接口
filds_count 有多少个字段
fields 有哪些字段,也就是属性啦
methods_count 方法有多少个
methods 方法都有些啥
attribute_count 参数个数
attribute 参数,里面最最重要的就是 code -- 代码
一个很棒的 IDEA 插件
十六进制数确实不是人看的,所以,在你的 IDEA 装个插件吧
直接在插件商店搜 jclasslib 就能找到它了,下它,使用很简单,代码里点一下你想看的类,然后 view -> shwo bytecode with jclasslib 就可以啦
是不是很棒,插件已经帮我们分析好了
内存加载大致的过程
- Loading 类加载器将字节码文件从硬盘加载到虚拟机内存
- Linking 包括以下三个步骤
verification 校验字节码文件是否合法,magic 是否为 cafe babe
preparation 静态变量赋默认值(注意不是你在代码里给定的初始值)
resolution 将符号引用解析为直接引用
- Initializing 静态变量赋初始值
JVM 内存模型简单过一遍
最后一步了,很重要哦。
Program Counter 程序计数器,简称 PC,用来放指令的位置,也就是说可以用它来找到下一条待执行的指令JVM Stacks 也就是 Java 虚拟机栈,内容有点多还是放到下面讲
Native Method Stacks 本地方法栈,也就是 C++ 这种底层的东西了,我们管不着
Heap 堆,对象的存放位置,所以也就是GC 的重点
Method Area 方法区,方法区其实只是一个概念,在 1.8 及之后的实现叫做 meta space
Direct Memory 直接内存
聊聊 JVM Stack
每个线程都会有一个 JVM Stack,每个 JVM Stack 都会有很多的栈帧 - Stack Frame,每个方法都对应着一个个的 Stack Frame 压在 JVM Stack 里头
每个 Frame 又有四个内容,是等等分析 i = i++ 的重点要掌握的,嗯,看我的 xmind 吧,懒得写了
开始分析 i = i++
来看这段小程序,如果时 j = i++,也就是先赋值后加加,相信你一定知道 j 肯定是 8,但现在是 i = i++,输出结果却是 8,下面我们通过字节码的层面来分析这个小程序(打开你的 jclasslib)
public static void main(String[] args) {
int i = 8;
i = i++;
// i = ++i;
System.out.println(i);
}
下面这是所谓的 JVM 指令,学过汇编的应该能看懂一些简单的指令,不单独讲了,想了解更多,直接点它,jclasslib 帮你直接跳转到官网,看看官网,就行了
我们来一个个指令的分析
bipush 8 将 8 压栈(操作数栈)istore_1,首先将 8 弹栈,然后放到局部变量表,等同于赋值,将 8 给了 i
iload_1, 再把 i 的值 8 又压到栈里
iinc 1 by 1 局部变量表 No.为 1 对应的数 +1,变成了 9
istore_1 然后又把栈里的 8 弹出赋值给了局部表量表的 1 位置,也就是覆盖了
因此,i=i++ 最后为8,下面的指令就是打印以及 return 了
再看看 i = ++i
不知道你看出来区别了没有,i = i++ 是先把 8 压栈,后面又弹出来覆盖了局部变量表
而这里是先iinc 1 by 1了,然后把 9 压栈,在重新赋值给 i,额,其实是多余的一次赋值,写成 ++i 就得了
还是简单解读以下这些指令8^_^
bipush -> b + i + push -> i很小byte能装下 + 变量i + 压栈
istore -> i + store -> 弹栈然后赋值给局部变量表的i
iinc 1 by 1 -> i + inc + 1 + by 1-> 局部变量表为1的位置加一
getstatic 我也没看过
invokevirtual 这个比较复杂,是 invoke指令的一种,学过反射的同学都应该知道 invoke 就是执行方法
- InvokeStatic 执行静态方法
- InvokeVirtual 是自带多态的一个指令,比如 List l = new ArrayList(),调用l.add()就是这个virtual了
- InvokeInterface 接口,
- InovkeSpecial 可以直接定位,不需要多态的方法 private 方法,构造方法
- InvokeDynamic JVM最难的指令 lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
嗯,没了,相信到这里你已经彻底懂了 i = i++ 这个无赖的梗了