字节码简介
1.计算机只能识别0 1 , 经过0 1的组合 , 产生了数字 , 0 1组合也产生了各种字符 , 各种机器指令
2.不同的时代 , 不同产商 , 机器指令集(arm , x86 , rsic ...)是不同的
3.CPU与指令集直接耦合 , 一个程序要在多个平台运行 , 需要多套代码
4.如何实现跨平台 , 中间码(字节码)应运而生
字节码实际应用
1.字节码是JVM里指令运行的形式
2.理解字节码对于编写高性能程序至关重要
3.一个不懂指令重排序的Java程序员 , 很难想象 , 能写出一个高性能的程序
类的加载
以常用的HotSpot JVM实现为例 , 默认将字节码解释执行 , 屏蔽对底层操作系统的依赖 , 祥转 JVM系列(一)
Java代码到字节码转化(编译)
1.切换到工作目录
2.书写Java代码
2.javac编译
3.尝试运行程序
4.查看编译后的字节码文件(笔者win7系统 , 用的ue) , 能看到以16进制显示的字节码文件
5.修改字节码
6.保存后再次运行
7.反编译字节码
字节码的主要构成
1.class文件段首cafe babe是Gosling定义的魔数 , 意思是Coffee Baby , 作用是标志该文件是一个Java类文件
2.红色标注的数字(00000034)为JDK的版本号 , 34的十进制为52 , 52是JDK8的内部版本号
3.纯数字的字节码难以阅读 , JVM在字节码上仿照汇编 , 设计了一套操作码助记符 , 使用特殊单词来标记这些数字 , 如ICONST_0代表00000011 , 即十六进制0x03 , ALOAD_0代表00101010 , 即0x2a ...
常用指令
1.加载、存储指令
- ILOAD(将int类型的局部变量压入栈) , ALOAD(将对象引用的局部变量压入栈)
- 从操作栈栈顶存储到局部变量表 , ISTORE、ASTORE ...
- 将常量加载到操作栈栈顶 , ICONST、BIPUSH、SIPUSH、LDC ...
2.运算指令
- 对两个操作栈顶上的值进行运算 , 并把结果写入操作栈顶 , IADD、IMUL ...
3.类型转换
- 显示转换两种不同的数值类型 , I2L、D2F ...
4.对象创建和访问指令
- 创建对象 , NEW、NEWARRAY ...
- 访问属性指令 , GETFIELD、PUTFIELD、GETSTATIC ...
- 检查实例类型指令 , INSTANCEOF、CHECKCAST ...
5.操作栈管理指令
- 出栈操作 , POP2 - 出栈两个元素
- 复制栈顶元素并压入栈 , DUP
6.方法调用与返回指令
- 调用对象的实例方法 , INVOKEVIRTUAL
- 调用实例初始化方法、私有方法、父类方法 , INVOKESPECIAL
- 调用类静态方法 , INVOKESTATIC
- 返回VOID类型 , RETURN
7.同步指令
- ACC_SYNCHRONIZED标志同步方法
- MONITORENTER、MONITOREXIT , 支持sycnhronized语义
指令重排序
1.一条指令的执行是分为多个步骤的 , 简单划分的话 , 可以分为 :
- IF 取值
- ID 译码和取寄存器操作数
- EX 执行或有效地址计算
- MEM 存储器访问
- WB 回写
2.每一步都可能使用不同的硬件完成 , IF时用到PC寄存器、存储器 , ID时用到指令寄存器组 , EX时用到ALU
3.因此 , 发明了流水线技术来执行指令
4.可以看到 , 当第二条指令执行时 , 第一条指令其实没有执行完 , 这样做的好处很明显 , 指令不再需要等待指令2执行完了 , 商业CPU的流水线级别可以达到10级以上 , 性能提升更加明显
5.但是 , 流水线并非可以一直这样满载的 , 如果被中端 , 所有硬件都会进入一个停顿期 , 再次满载又需要几个周期 , 所以要尽可能不让流水线中断
6.之所以做重排序 , 就是为了尽量少的中断指令流水线
7.举个简单的例子(只列出主要部分) , c=b+a
8.add中断的原因很简单 , 因为'b'的数据还没有准备好 , 理解了这个例子 , 再看一个更复杂的情况
9.由于ADD和SUB都需要等待上一条指令的结果 , 所以在这里有不少的中断
10.这时候 , 指令重排登场了
11.重排后的指令 , 流水线完美执行
12.重排序可能导致的问题 , 看下面这个经典单例
13.我们通过上面的知识 , 查看字节码
14.可以看到创建对象demo
- 17 : new指令在java堆上为demo对象分配内存空间 , 并将地址压入操作栈顶
- 20 : dup指令为复制操作栈顶值 , 并将其压入栈顶 , 这时操作栈上有连续相同的两个对象地址
- 21 : 调用实例的构造函数 , 这一步会弹出一个之前入栈的对象地址
- 24 : 将对象地址赋值给demo
15.由上可得 , 创建一个对象并非原子操作 , 重排序后 , 21如果在24之后 , 就可能导致其它线程拿到未初始化的实例 , 造成不必要的问题
16.解决这个问题的方式也很简单 , 只需要为demo变量加上volatile关键字即可 , 具体原因 , 将在后续章节给出