class 文件结构

u1 表示 1 字节无符号

u2 表示 2 字节无符号

u4 表示 4 字节无符号

官方文档 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

magic number

查看

hexdump -C 文件

class 文件的 magic number 为 0xCAFEBABE

常量池

// 假设它的大小为 N, 则它的有效索引是 `[1, N-1]` . 注意, `long` 和 `double` 会占用两个索引的位置.
struct {
    u2  constant_pool_count;
    cp_info constant_pool[constant_pool_count1];
}

cp_info {
    u1 tag;
    u1 info[];
}

tag 的类型有14 种

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

各种类型的表示

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}

CONSTANT_Float_info {
    u1 tag;
    u4 bytes;
}

CONSTANT_Long_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

CONSTANT_Double_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

// 它存储的是 Modified UTF8 而非 UTF8 字符串的内容
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

// 它存储的是常量池的索引
CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_InterfaceMethodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

CONSTANT_MethodHandle_info {
    u1 tag;
    u1 reference_kind;
    u2 reference_index;
}

CONSTANT_MethodType_info {
    u1 tag;
    u2 descriptor_index;
}

CONSTANT_InvokeDynamic_info {
    u1 tag;
    u2 bootstrap_method_attr_index;
    u2 name_and_type_index;
}

Access flags

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

this_class , super_name, interfaces

它们的值都是常量池中对应的索引值

fields

struct {
  u2	fields_count;
  field_info fields[fields_count];
}

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

Access flag (访问标识)的值

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; usable only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; never directly assigned to after object construction.
ACC_VOLATILE 0x0040 Declared volatile; cannot be cached.
ACC_TRANSIENT 0x0080 Declared transient; not written or read by a persistent object manager.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ENUM 0x4000 Declared as an element of an enum.

Descriptor (字段描述符) 的值

FieldType term Type Interpretation
B byte signed byte
C char Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L ClassName ; reference an instance of class ClassName
S short signed short
Z boolean true or false
[ reference one array dimension

methods

struct {
  u2	methods_count;
  method_info methods[methods_count];
}

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

Access flags (访问标识)

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; must not be overridden.
ACC_SYNCHRONIZED 0x0020 Declared synchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 A bridge method, generated by the compiler.
ACC_VARARGS 0x0080 Declared with variable number of arguments.
ACC_NATIVE 0x0100 Declared native; implemented in a language other than Java.
ACC_ABSTRACT 0x0400 Declared abstract; no implementation is provided.
ACC_STRICT 0x0800 Declared strictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

attributes

struct {
	u2	attributes_count;
	attribute_info attributes[attributes_count];
}

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

class 文件中常见的属性

  • ConstantValue : 用来表示常量值
  • Code : 它包含方法的字节码
  • StackMapTable
  • Exceptions
  • BootstrapMethods

字节码

它使用的是 Big-endian 大端表示.

虚拟机栈和栈帧

  • 基于栈: 移植性好, 指令更短, 实现简单, 但不能随机访问栈中的元素. 实现同样的功能, 需要的指令数比寄存器多. 要频繁地入栈出栈.
    • Hotspot JVM
  • 基于寄存器: 速度快, 充分利用寄存器. 但操作数需要显式指定, 指令较长.
    • DalvikVM(android)

栈帧

  • 局部变量表
  • 操作数栈
  • 指向常量池的引用

    javac -g xxx.java
    
    javap -c -v -p -l xxx
    

指令

可参考另一篇笔记: JVM字节码学习与理解

加载和存储指令

  • load
    • lload
    • fload
    • dload
    • aload : 加载引用类型
  • store
    • lstore
    • fstore
    • dstore
    • astore
  • 常量加载
    • const : 将常量值直接加载到操作数栈
    • push : 将常量值直接加载到操作数栈
    • ldc : 从常量池加载对应的常量到操作数栈

int 型常量根据不同的范围, 有不用的类型

  • [-1, 5] : 使用 iconst_n 的方式. -1 对应的是 iconst_m1
  • [-128, 127] : 使用 bipush n 的方式
  • [-32768, 32767] : 使用 sipush n 的方式
  • 其他范围, 则使用 ldc 的方式

操作数栈指令

  • pop
  • dup
  • swap

运算和类型转换指令

运算(其他类推)

  • iadd
  • isub
  • idiv
  • imul
  • irem
  • ineg
  • iand
  • ior
  • ixor

转换(其他类推)

  • i2f
  • i2b

控制转移指令

  • ifle
  • Ifge

等等

switch-case 底层实现

lookupswitch : 在 case 稀疏情况下使用

lookuptable : 在 case 紧凑的情况下使用

switch String 的字节码

    public int switchString(final String name) {
        switch (name) {
            case "Java":
                return 100;
            case "Kolin":
                return 101;
            default:
                return -1;
        }
    }

它的反编译代码为

    public int switchString(String name) {
        byte var3 = -1;
        switch(name.hashCode()) {
        case 2301506:
            if (name.equals("Java")) {
                var3 = 0;
            }
            break;
        case 72678029:
            if (name.equals("Kolin")) {
                var3 = 1;
            }
        }

        switch(var3) {
        case 0:
            return 100;
        case 1:
            return 101;
        default:
            return -1;
        }
    }

生成的字节码为

  public int switchString(java.lang.String);
    descriptor: (Ljava/lang/String;)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_1
         1: astore_2
         2: iconst_m1
         3: istore_3
         4: aload_2
         5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
         8: lookupswitch  { // 2
                 2301506: 36
                72678029: 50
                 default: 61
            }
        36: aload_2
        37: ldc           #3                  // String Java
        39: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
        42: ifeq          61
        45: iconst_0
        46: istore_3
        47: goto          61
        50: aload_2
        51: ldc           #5                  // String Kolin
        53: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
        56: ifeq          61
        59: iconst_1
        60: istore_3
        61: iload_3
        62: lookupswitch  { // 2
                       0: 88
                       1: 91
                 default: 94
            }
        88: bipush        100
        90: ireturn
        91: bipush        101
        93: ireturn
        94: iconst_m1
        95: ireturn


      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      96     0  this   Lio/emacsist/github/pdf/TestConst;
            0      96     1  name   Ljava/lang/String;

  • 先调用 hashCode
  • 根据 hashCode 跳转指定位置
  • 再调用 equals 判断是否相等, 然后赋值到一个临时变量
  • 再根据这个临时变量, 按普通的 switch 的 int 的形式调用

try finally 的实现

public class TestConst {
    public void testFinally() {
        try {
            testThrow();
        } catch (final Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("finally");
        }
    }

    private void testThrow() {
        final File f = new File("/tmp/hello.txt");
        System.out.println(f.exists());
    }
}

它生成的字节码

  public void testFinally();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method testThrow:()V
         4: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #4                  // String finally
         9: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: goto          42
        15: astore_1
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/Exception.printStackTrace:()V
        20: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        23: ldc           #4                  // String finally
        25: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: goto          42
        31: astore_2
        32: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        35: ldc           #4                  // String finally
        37: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        40: aload_2
        41: athrow
        42: return
      Exception table:
         from    to  target type
             0     4    15   Class java/lang/Exception
             0     4    31   any
            15    20    31   any
      LineNumberTable:
        line 8: 0
        line 12: 4
        line 13: 12
        line 9: 15
        line 10: 16
        line 12: 20
        line 13: 28
        line 12: 31
        line 13: 40
        line 14: 42
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           16       4     1     e   Ljava/lang/Exception;
            0      43     0  this   Lio/emacsist/github/pdf/TestConst;
      StackMapTable: number_of_entries = 3
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 10 /* same */

finally 的实现方式是在每个可能的路径上的最后的代码插入 finally 的代码, 这样子就可以保证 finally 的代码都会被执行(可以看到, 上面的 print finally 显示了 3 次).

这样子, 就可以理解, 如果在 try 及 finally 中都写了 return 的结果了.

如果在最后的 finally 赋值了, 比如下面

    private static String testFinally() {
        String s = "hello";
        try {
            return s;
        } catch (final Exception e) {
            return "excepto";
        } finally {
            s = "finally";
        }
    }

它的反编译代码为

    private static String testFinally() {
        String s = "hello";

        String var2;
        try {
            String var1 = s;
            return var1;
        } catch (Exception var6) {
            var2 = "excepto";
        } finally {
            s = "finally";
        }

        return var2;
    }

try resource 实现

    private static void tryResource() {
        try (final FileWriter f = new FileWriter("/tmp/hello.txt")) {
            f.write("hello");
            f.flush();
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }

它生成的字节码为

  private static void tryResource();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=0
         0: new           #2                  // class java/io/FileWriter
         3: dup
         4: ldc           #3                  // String /tmp/hello.txt
         6: invokespecial #4                  // Method java/io/FileWriter."<init>":(Ljava/lang/String;)V
         9: astore_0
        10: aconst_null
        11: astore_1
        12: aload_0
        13: ldc           #5                  // String hello
        15: invokevirtual #6                  // Method java/io/FileWriter.write:(Ljava/lang/String;)V
        18: aload_0
        19: invokevirtual #7                  // Method java/io/FileWriter.flush:()V
        22: aload_0
        23: ifnull        91
        26: aload_1
        27: ifnull        46
        30: aload_0
        31: invokevirtual #8                  // Method java/io/FileWriter.close:()V
        34: goto          91
        37: astore_2
        38: aload_1
        39: aload_2
        40: invokevirtual #10                 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
        43: goto          91
        46: aload_0
        47: invokevirtual #8                  // Method java/io/FileWriter.close:()V
        50: goto          91
        53: astore_2
        54: aload_2
        55: astore_1
        56: aload_2
        57: athrow
        58: astore_3
        59: aload_0
        60: ifnull        89
        63: aload_1
        64: ifnull        85
        67: aload_0
        68: invokevirtual #8                  // Method java/io/FileWriter.close:()V
        71: goto          89
        74: astore        4
        76: aload_1
        77: aload         4
        79: invokevirtual #10                 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
        82: goto          89
        85: aload_0
        86: invokevirtual #8                  // Method java/io/FileWriter.close:()V
        89: aload_3
        90: athrow
        91: goto          99
        94: astore_0
        95: aload_0
        96: invokevirtual #12                 // Method java/io/IOException.printStackTrace:()V
        99: return
      Exception table:
         from    to  target type
            30    34    37   Class java/lang/Throwable
            12    22    53   Class java/lang/Throwable
            12    22    58   any
            67    71    74   Class java/lang/Throwable
            53    59    58   any
             0    91    94   Class java/io/IOException
      LineNumberTable:
        line 9: 0
        line 10: 12
        line 11: 18
        line 12: 22
        line 9: 53
        line 12: 58
        line 14: 91
        line 12: 94
        line 13: 95
        line 15: 99
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      81     0     f   Ljava/io/FileWriter;
           95       4     0     e   Ljava/io/IOException;
      StackMapTable: number_of_entries = 10
        frame_type = 255 /* full_frame */
          offset_delta = 37
          locals = [ class java/io/FileWriter, class java/lang/Throwable ]
          stack = [ class java/lang/Throwable ]
        frame_type = 8 /* same */
        frame_type = 70 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 255 /* full_frame */
          offset_delta = 15
          locals = [ class java/io/FileWriter, class java/lang/Throwable, top, class java/lang/Throwable ]
          stack = [ class java/lang/Throwable ]
        frame_type = 10 /* same */
        frame_type = 3 /* same */
        frame_type = 255 /* full_frame */
          offset_delta = 1
          locals = []
          stack = []
        frame_type = 66 /* same_locals_1_stack_item */
          stack = [ class java/io/IOException ]
        frame_type = 4 /* same */

它的反编译代码为

    private static void tryResource() {
        try {
            FileWriter f = new FileWriter("/tmp/hello.txt");
            Throwable var1 = null;

            try {
                f.write("hello");
                f.flush();
            } catch (Throwable var11) {
                var1 = var11;
                throw var11;
            } finally {
                if (f != null) {
                    if (var1 != null) {
                        try {
                            f.close();
                        } catch (Throwable var10) {
                            var1.addSuppressed(var10);
                        }
                    } else {
                        f.close();
                    }
                }

            }
        } catch (IOException var13) {
            var13.printStackTrace();
        }

    }

对象相关的字节码指令

  • <init> 方法. 对象初始化方法, 类的构造方法, 非静态变量的初始化, 对象初始化代码块都会放在这里
  • 创建一个对象, 要用三条指令

    public void testNew() {
        final Object o = new Object();
    }
    

对应生成的字节码为

  public void testNew();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lio/emacsist/github/pdf/TestConst;
            8       1     1     o   Ljava/lang/Object;

  • new : 只是创建了一个类实例的引用, 然后压入操作数栈
  • invokespecial : 调用 <init> 方法来调用构造函数初始化.
  • dup : 因为在调用 invokespecial 后, 它会消耗操作数栈顶的实例引用. 如果不调用 dup 后再调用 invokespecial 的话, 则调用完后, 操作数栈顶就没有这个实例引用了. 因为<init> 方法并没有返回值

    • <clinit> : 静态初始化方法, 类静态初始化块, 静态变量初始化都会在这里. 在字节码里表示为 static {}

      public class TestConst {
      public static int a = 10;
          
      static {
      System.out.println("hello");
      }
      
      public static int b = 20;
      }
      
      

它生成的字节码为

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field a:I
         5: bipush        20
         7: putstatic     #3                  // Field b:I
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: ldc           #5                  // String hello
        15: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        18: return
      LineNumberTable:
        line 4: 0
        line 5: 5
        line 9: 10
        line 10: 18

字节码进阶

方法调用指令

  • invokestatic : 调用静态方法
    • 不需要将对象加载到操作数栈
    • 只需要将参数入栈, 然后就直接调用该指令
  • invokespecial : 调用私有实例方法, 构造器方法以及 super 调用父类实例方法
    • 实例构造方法 <init>
    • private 修饰的私有实例方法
    • super 调用的父类方法
  • invokevirtual : 调用非私有实例方法
    • 需要将对象, 方法参数入栈
    • 调用结束时, 对象引用, 方法参数都会出栈
    • 如果有返回值, 则返回值入栈
  • invokeinterface : 调用接口方法
  • invokedynamic : 调用动态方法. 调用流程:
    • 首次执行该指令时, 会调用 bootstrap method
    • 上面的方法返回一个 CallSite 对象. CallSite 内部根据方法签名进行目标方法查找. 它的 getTarget 方法返回方法句柄 (MethodHandle ) 对象
    • 在 CallSite 没变化的情况下, MethodHandle 可以一直调用. 如果有变化, 则重新查找.

vtable : 提供 invokevirtual 指令调用方法定位表.

  • finalstatic 修饰的方法不会出现在 vtable
  • 子类会继承父类的 vtable
  • Object 类有 5 个虚方法. 所以一个对象的 vtable 至少是 5
    • hashCode
    • equals
    • clone
    • toString
    • finalize

itable : 提供 invokeinterface : 指令 调用方法定位表

Lambda 表达式的原理

匿名内部类在编译期间生成新的 class 文件来实现的.

而 lambda 而是使用 invokedynamic 来实现的

 0: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;

invokedynamic 指令后面的 #N 表示从 BootstrapMethods: 中的N 索引中的方法. 对应的是调用

    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
  • caller : 表示 JVM 提供的查找上下文
  • invokedName : 表示调用函数名
  • samMethodType : 函数式接口定义的方法签名
  • implMethod : 编译时生成的 Lambda 表达式对应的静态方法
  • instantiatedMethodType : 一般和 samMethodType 是一样或是它的一个特例

整个流程如下

  • Lambda 的地方会生成一个 invokeddynamic 指令, 同时编译器生成一个对应的 bootstrap method
  • 第一次执行 invokeddynamic 指令时, 会调用对应的 bootstrap method, 它会调用 LambdaMetafactory.metafactory 方法动态生成内部类
  • bootstrap method 会返回一个动态调用 CallSite 对象, 它会最终调用实现了接口的内部类
  • Lambda 表达式中的内容会被编译成静态方法, 前面生成的动态内部类会直接调用该方法
  • 真正执行 lambda 调用的还是调用 invokeinterface 指令

要想获取运行期生成的内部类, 可以添加 JVM 参数 -Djdk.internal.lambda.dumpProxyClasses=<path to dump directory> 即可.

package io.emacsist.github.pdf;

public class TestConst {
    public static void main(String[] args) {
        Runnable r1 = () -> {
            System.out.println("lambda");
        };
        r1.run();
    }
}

然后运行 java -Djdk.internal.lambda.dumpProxyClasses=/tmp/lambda/ io.emacsist.github.pdf.TestConst

最后, 可以在目录 /tmp/lambda 中看到生成的内部类了

tree /tmp/lambda   
/tmp/lambda
└── io
    └── emacsist
        └── github
            └── pdf
                └── TestConst$$Lambda$1.class

反编译生的代码为

final class io.emacsist.github.pdf.TestConst$$Lambda$1 implements java.lang.Runnable {
  private io.emacsist.github.pdf.TestConst$$Lambda$1();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: return

  public void run();
    Code:
       0: invokestatic  #17                 // Method io/emacsist/github/pdf/TestConst.lambda$main$0:()V
       3: return
}

可以看到, 新生成的内部类的 run 方法就直接调用了静态方法.

泛型与字节码

package io.emacsist.github.pdf;

import java.util.List;

public class TestConst {
    public void say(List<String> hello) {
        String a = hello.get(0);
        System.out.println(a);
    }
}

生成的字节码为

  public void say(java.util.List<java.lang.String>);
    descriptor: (Ljava/util/List;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: aload_1
         1: iconst_0
         2: invokeinterface #2,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
         7: checkcast     #3                  // class java/lang/String
        10: astore_2
        11: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        14: aload_2
        15: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        18: return
      LineNumberTable:
        line 8: 0
        line 9: 11
        line 10: 18
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  this   Lio/emacsist/github/pdf/TestConst;
            0      19     1 hello   Ljava/util/List;
           11       8     2     a   Ljava/lang/String;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0      19     1 hello   Ljava/util/List<Ljava/lang/String;>;
    Signature: #24                          // (Ljava/util/List<Ljava/lang/String;>;)V
  • 泛型附加信息对 JVM 来说是不可见的. 泛型是在 Javac 中实现的, 生成的字节码中, 已经不包含泛型信息. 在使用时加上类型, 编译时再抹掉, 这称为泛型擦除
  • 泛型并没有自己独有的 class 类对象
  • 泛型不能用 primary 原始类型来实例化参数
  • Java 不能捕获泛型异常
  • 不能声明泛型数组

synchronized 的字节码

package io.emacsist.github.pdf;

public class TestConst {

    int i = 0;

    public void incr() {
        synchronized (this) {
            i++;
        }
    }
}

生成的字节码为

  public void incr();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field i:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumberTable:
        line 8: 0
        line 9: 4
        line 10: 14
        line 11: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  this   Lio/emacsist/github/pdf/TestConst;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class io/emacsist/github/pdf/TestConst, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

可以看到, 由编译器生成的字节码, 会保证 monitorenter 后, 一定会执行 monitorexit

反射

Inflation 机制 . https://blogs.oracle.com/buck/inflation-system-properties

它由两个参数影响

  • -Dsun.reflect.inflationThreshold=N : 默认为 15. 表示反射的方法将会有 N 次调用是通过 JNI 的方式实现的
  • -Dsun.reflect.noInflation=true|false : 默认为 false . 如果为 true, 则直接使用动态生成的类的方式来调用反射

javac 编译原理

7 个阶段

  1. parse : 词法和语法分析.
    1. 词法分析: com.sun.tools.javac.parser.Scanner 实现
    2. 语法分析: com.sun.tools.javac.parser.JavacParser 实现
  2. enter : 解析和填充符号表. 没默认构造函数, 则添加默认. 主要由以下实现
    1. com.sun.tools.javac.comp.Enter
    2. com.sun.tools.javac.comp.MemberEnter
  3. process : 注解处理. 由 com.sun.tools.javac.processing.JavacProcessingEnvironment 完成
  4. attribute : 做语义合法性检查, 常量折叠等.
  5. flow : 处理数据流分析. 主要由 com.sun.tools.javac.comp.Flow 类实现.
    1. 非 void 方法所有退出分支是否都有返回值
    2. 检查受检异常是否被捕获或显式抛出
    3. 检查局部变量是否被初始化
    4. 检查 final 是否有重复赋值
    5. 检查语句是否有不可达
  6. desugar : 解除语法糖
  7. generate : 生成 class 文件. 由 com.sun.tools.javac.jvm.Gen 类实现

switch 的选择

// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
    nlabels > 0 &&
    table_space_cost + 3 * table_time_cost <=
    lookup_space_cost + 3 * lookup_time_cost
    ?
    tableswitch : lookupswitch;

选择 tableswitch O(1)还是 lookupswitch O(log n) , 是根据上面的”代价”来选择的

  • hi 表示 case 的最大值
  • lo 表示 case 的最小值

字节码操作工具

asm

https://asm.ow2.io/

性能最好, 但要求高

javassist

https://www.javassist.org/

相对简单点. 但性能不如 asm

Instrumentation

使用方式

  • 通过 agent : java -javaagent:myagent.jar MyMain . 它要在 jar 包的 MANIFEST.MF 文件指定相关的信息
  • 基于 attach 方式:

    VirtualMachine jvm = VirtualMachine.attach(jvmPid);
    jvm.loadAgent(agentFile.getAbsolutePath());
    jvm.detach();
    

创建一个 agent

  • 创建一个类, 假设为 MyAgent
  • 添加一个静态方法, 签名为 public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception
    • 第一个参数是 agent 的启动参数. 写法如下 java -javaagent:xxxx.jar=key:value,key2:value2
    • 第二个参数是 instrumentation 的实例
    • 添加一个类 MyClassFileTransformer 实现 ClassFileTransformer 接口进行 class 文件转换
    • 然后在 instrumentation 实例中调用 addTransformer(new MyClassFileTransformer(), true) 即可
  • 打包. (要添加 manifest 信息)

    <plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.1.2</version>
        <configuration>
            <archive>
                <manifestEntries>
                    <Premain-Class>xxx.MyAgent</Premain-Class>
                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                </manifestEntries>
            </archive>
        </configuration>
    </plugin>
    </plugins>
    

Attach 的方式的话, 它调用的是 public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception

参考资料

https://www.throwable.club/2019/06/29/java-understand-instrument-first/

JSR 269 插件化注解处理原理

在前面的 javac 阶段中的 process 阶段. JSR 269 就发生在这阶段.

字节码的应用

cglib

asm : 字节码改写

cglib : 动态代理

编写步骤

  • 实现 MethodInterceptor
  • 调用 Enhancer.create
  • 通过代理类实例调用目标方法

要想查找 cglib 生成的类, 可以设置属性让 cglib 保存到磁盘文件中

-Dcglib.debugLocation=/tmp/cglib/

cglib 不同于 JDK 动态代理使用反射来调用被拦截的方式, cglib 使用 FastClass 来避免反射. 它的原理是对被代理类的方法增加索引, 通过索引来定位具体方法.

fastjson

使用 asm 为对应的 bean 生成类, 规避反射获取类字段的操作

dubbo

Javassit 生成接口的代理类

jacoco

使用 asm 实现覆盖率统计功能

mockito

使用 cglib 生成子类实现 mock 的功能