一、反编译class文件,获取字节码清单

1.1 编译 .java文件,得到 .class文件

  • 编写java文件

    1. public class App {
    2. public static void main(String[] args) {
    3. System.out.println("Hello World!");
    4. }
    5. }
  • 编译java文件,得到class文件

    1. # javac -help 查看用法
    2. # -encoding utf-8 指定编码
    3. # -g 在反编译时,会有局部变量表
    4. javac -encoding utf-8 App.java
  • 查看class文件,16进制格式(使用IDEA的HEX View插件)

image.png

1.2 javap 反编译class文件, 获取字节码清单

  • javap 的使用帮助

    1. javap -help
  • 反编译, 输出到文件App.javap

    1. javap -verbose -p App.class > App.javap
  • 打开App.javap文件

    1. Classfile /D:/IDEA/demo/javalearn/src/main/java/cn/hdj/App.class
    2. Last modified 2020-10-12; size 419 bytes
    3. MD5 checksum 61d336aa9be2ce70eff27134d076f642
    4. Compiled from "App.java"
    5. public class cn.hdj.App
    6. minor version: 0
    7. major version: 55
    8. flags: ACC_PUBLIC, ACC_SUPER
    9. Constant pool:
    10. #1 = Methodref #6.#15 // java/lang/Object."<init>":()V
    11. #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
    12. #3 = String #18 // Hello World!
    13. #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
    14. #5 = Class #21 // cn/hdj/App
    15. #6 = Class #22 // java/lang/Object
    16. #7 = Utf8 <init>
    17. #8 = Utf8 ()V
    18. #9 = Utf8 Code
    19. #10 = Utf8 LineNumberTable
    20. #11 = Utf8 main
    21. #12 = Utf8 ([Ljava/lang/String;)V
    22. #13 = Utf8 SourceFile
    23. #14 = Utf8 App.java
    24. #15 = NameAndType #7:#8 // "<init>":()V
    25. #16 = Class #23 // java/lang/System
    26. #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
    27. #18 = Utf8 Hello World!
    28. #19 = Class #26 // java/io/PrintStream
    29. #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
    30. #21 = Utf8 cn/hdj/App
    31. #22 = Utf8 java/lang/Object
    32. #23 = Utf8 java/lang/System
    33. #24 = Utf8 out
    34. #25 = Utf8 Ljava/io/PrintStream;
    35. #26 = Utf8 java/io/PrintStream
    36. #27 = Utf8 println
    37. #28 = Utf8 (Ljava/lang/String;)V
    38. {
    39. public cn.hdj.App();
    40. descriptor: ()V
    41. flags: ACC_PUBLIC
    42. Code:
    43. stack=1, locals=1, args_size=1
    44. 0: aload_0
    45. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
    46. 4: return
    47. LineNumberTable:
    48. line 8: 0
    49. public static void main(java.lang.String[]);
    50. descriptor: ([Ljava/lang/String;)V
    51. flags: ACC_PUBLIC, ACC_STATIC
    52. Code:
    53. stack=2, locals=1, args_size=1
    54. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
    55. 3: ldc #3 // String Hello World!
    56. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    57. 8: return
    58. LineNumberTable:
    59. line 10: 0
    60. line 11: 8
    61. }
    62. SourceFile: "App.java"

二、 字节码了解

2.1 字节码格式

在了解字节码文件信息前,我们需要先了解class文件格式有哪些组成?

  • 一个class文件组成

    1. ClassFile {
    2. u4 magic; //魔数, 用于识别class文件格式
    3. u2 minor_version;//次版本号
    4. u2 major_version;//主版本号
    5. u2 constant_pool_count; //常量池计数器
    6. cp_info constant_pool[constant_pool_count-1]; //常量池
    7. u2 access_flags;//访问标志
    8. u2 this_class;//当前类名
    9. u2 super_class;//父类名称
    10. u2 interfaces_count;//接口计数器
    11. u2 interfaces[interfaces_count];//接口信息
    12. u2 fields_count;//字段计数器
    13. field_info fields[fields_count];//字段表
    14. u2 methods_count;//方法计数器
    15. method_info methods[methods_count];//方法表
    16. u2 attributes_count; //属性计数器
    17. attribute_info attributes[attributes_count];附加属性表
    18. }
  • 访问标志 | 标识符名称 | 标识符值 | 描述 | | —- | —- | —- | | ACC_PUBLIC | 0x0001 | 声明 public ,能在外部其它包中访问 | | ACC_FINAL | 0x0010 | 声明为final;只用于类,标识不能创建子类 | | ACC_SUPER | 0x0020 | 可通过invokespecial指令调用超类的方法 | | ACC_INTERFACE | 0x0200 | 标志这是一个接口 | | ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,
    次标志值为真,其他类型为假 | | ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 | | ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | | ACC_ENUM | 0x4000 | 标志这是一个枚举 |

  • 字节码的类型对应 | 字段术语 | 代表类型 | 解析 | | —- | —- | —- | | B | byte | 基本类型byte | | C | char | 基本类型char | | D | double | 基本类型double | | F | float | 基本类型float | | I | int | 基本类型int | | J | long | 基本类型long | | L ClassName | reference | 对象类型,以分号结尾,如Ljava/lang/Object; | | S | short | 基本类型short | | Z | boolean | 基本类型布尔值;true 或者 false | | [ | reference | 表示一维数组引用 | | V | void | 特殊类型void |
  • 算数操作与类型转换

image.png

  • 方法调用的指令 | 指令 | 描述 | | —- | —- | | invokestatic | 用于调用某个类的静态方法 | | invokespecial | 用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。 | | invokevirtual | 如果是具体类型的目标对象,invokevirtual 用于调用公共,受保护和package 级的私有方法。 | | invokeinterface | 当通过接口引用来调用方法时,将会编译invokeinterface 指令。 | | invokedynamic | JDK7 新增加的指令,是实现“动态类型语言”(Dynamically TypedLanguage)支持而进行的升级改进,同时也是 JDK8 以后支持 lambda 表达式的实现基础。 |

2.3 解读字节码

  • 源码文件

    1. public class DemoDynamic {
    2. public static void foo() {
    3. int a = 1;
    4. int b = 2;
    5. int c = (a + b) * 5;
    6. }
    7. }
  • 反编译

    1. javac -g -encoding utf-8 DemoDynamic.java
    2. javap -verbose -c .\DemoDynamic.class > .\DemoDynamic.javap
  • DemoDynamic.javap 反编译文件

头部信息

  1. Classfile /D:/IDEA/Java-Learning/src/main/java/cn/hdj/jvm/bytecode/DemoDynamic.class
  2. Last modified 2020-10-17; size 419 bytes
  3. MD5 checksum 0242e2d86e94eb62d302f5a034336416
  4. Compiled from "DemoDynamic.java"
  5. public class cn.hdj.jvm.bytecode.DemoDynamic
  6. minor version: 0 //版本号
  7. major version: 52
  8. flags: ACC_PUBLIC, ACC_SUPER 访问标识符

常量池信息

  1. //常量池, 常量池可以理解成Class文件中的资源仓库
  2. //主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)
  3. Constant pool:
  4. // #1常量是一个方法定义,,指向了第3和第18个常量,注释为该常量的内容
  5. // 注释的内容可以理解为构造函数的初始化,执行的是Object对象的<init>方法,没有参数,无返回值
  6. #1 = Methodref #3.#18 // java/lang/Object."<init>":()V
  7. #2 = Class #19 // cn/hdj/jvm/bytecode/DemoDynamic
  8. #3 = Class #20 // java/lang/Object
  9. #4 = Utf8 <init>
  10. #5 = Utf8 ()V
  11. #6 = Utf8 Code
  12. #7 = Utf8 LineNumberTable
  13. #8 = Utf8 LocalVariableTable
  14. #9 = Utf8 this
  15. #10 = Utf8 Lcn/hdj/jvm/bytecode/DemoDynamic;
  16. #11 = Utf8 foo
  17. #12 = Utf8 a
  18. #13 = Utf8 I //通过字节码类型可知,该常量为int类型
  19. #14 = Utf8 b
  20. #15 = Utf8 c
  21. #16 = Utf8 SourceFile
  22. #17 = Utf8 DemoDynamic.java
  23. #18 = NameAndType #4:#5 // "<init>":()V
  24. #19 = Utf8 cn/hdj/jvm/bytecode/DemoDynamic
  25. #20 = Utf8 java/lang/Object

foo() 方法

  1. public static void foo();
  2. descriptor: ()V
  3. flags: ACC_PUBLIC, ACC_STATIC //标识符,public static
  4. Code:
  5. stack=2, locals=3, args_size=0 //栈容量2 , 局部变量表容量3, 参数个数0
  6. 0: iconst_1 // 加载int型1到栈顶 -> 栈1=1
  7. 1: istore_0 // 把栈顶 1 值存到局部变量表0位置 -> 局部0=1
  8. 2: iconst_2 // 加载int型2到栈顶位置 -> 栈1=2
  9. 3: istore_1 // 把栈2值存到局部变量表1位置 -> 局部1=2
  10. 4: iload_0 // 从局部变量表中加载1,入栈
  11. 5: iload_1 // 从局部变量表中加载2,入栈
  12. 6: iadd //执行相加操作, 1+2 = 3, 入栈
  13. 7: iconst_5 //加载int型5,入栈
  14. 8: imul //执行相乘操作,3*5=15,入栈
  15. 9: istore_2 // 保存到局部变量表2位置,-> 局部2=15
  16. 10: return //返回
  17. LineNumberTable: //行数表
  18. line 9: 0
  19. line 10: 2
  20. line 11: 4
  21. line 12: 10
  22. LocalVariableTable: //局部变量表
  23. Start Length Slot Name Signature
  24. 2 9 0 a I
  25. 4 7 1 b I
  26. 10 1 2 c I
  27. }
  • stack 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1
  • locals:局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。
  • args_size: 方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this
  • LocalVariableTable : 该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。start 表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。
  • foo()方法字节码执行图示

byte-code.gif

三、字节码增强

3.1 ASM

3.2 Javassist

辅助工具