JVM系列 : 字节码、指令、重排序

字节码简介

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关键字即可 , 具体原因 , 将在后续章节给出

展开阅读全文

没有更多推荐了,返回首页