上级目录

ClassFile 中的 StackMapTable

在 ClassFile 结构中,有一个 StackMapTable 结构:

  • 在 ClassFile 结构中,每一个方法都对应于 method_info 结构;
  • 在 method_info 结构中,方法体的代码存储在 Code 结构内;
  • 在 Code 结构中,frame 的变化存储在 StackMapTable 结构中。

假如有一个 HelloWorld 类,内容如下:

public class HelloWorld {
    public void test(boolean flag) {
        if (flag) {
            System.out.println("value is true");
        }
        else {
            System.out.println("value is false");
        }
    }
}

查看 Instruction

在 .class 文件中,方法体的内容会被编译成一条一条的 instruction。我们可以通过使用 javap -c sample.HelloWorld 来查看 Instruction 的内容。

public void test(boolean);
  Code:
     0: iload_1
     1: ifeq          15
     4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
     7: ldc           #3                  // String value is true
     9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: goto          23
    15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    18: ldc           #5                  // String value is false
    20: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    23: return

查看 Frame

在方法当中,每一条 Instruction 都有对应的 Frame。

我们可以通过运行 HelloWorldFrameCore 来查看 Frame 的具体情况:

test(Z)V
[sample/HelloWorld, int] []
[sample/HelloWorld, int] [int]
[sample/HelloWorld, int] []
[sample/HelloWorld, int] [java/io/PrintStream]
[sample/HelloWorld, int] [java/io/PrintStream, java/lang/String]
[sample/HelloWorld, int] []
[] []
[sample/HelloWorld, int] [java/io/PrintStream]
[sample/HelloWorld, int] [java/io/PrintStream, java/lang/String]
[sample/HelloWorld, int] []
[] []

或者运行 HelloWorldFrameCore02 来查看 Frame 的具体情况:

test:(Z)V
                               // {this, int} | {}
0000: iload_1                  // {this, int} | {int}
0001: ifeq            14       // {this, int} | {}
0004: getstatic       #2       // {this, int} | {PrintStream}
0007: ldc             #3       // {this, int} | {PrintStream, String}
0009: invokevirtual   #4       // {this, int} | {}
0012: goto            11       // {} | {}
                               // {this, int} | {}
0015: getstatic       #2       // {this, int} | {PrintStream}
0018: ldc             #5       // {this, int} | {PrintStream, String}
0020: invokevirtual   #4       // {this, int} | {}
                               // {this, int} | {}
0023: return                   // {} | {}

严格的来说,每一条 Instruction 都对应两个 frame,一个是 instruction 执行之前的 frame,另一个是 instruction 执行之后的 frame。但是,当多个 instruction 放到一起的时候来说,第 n 个 instruction 执行之后的 frame,就成为第 n+1 个 instruction 执行之前的 frame,所以也可以理解成:每一条 instruction 对应一个 frame。

这些 frames 是要存储起来的。我们知道,每一个 instruction 对应一个 frame,如果都要存储起来,那么在 .class 文件中就会占用非常多的空间;而 .class 文件设计的一个主要目标就是尽量占用较小的存储空间,那么就需要对这些 frames 进行压缩。

压缩 frames

为了让 .class 文件占用的存储空间尽可能的小,因此要对 frames 进行压缩。

对 frames 进行压缩,从本质上来说,就是忽略掉一些不重要的 frames,而只留下一些重要的 frames。

那么,怎样区分哪些 frames 重要,哪些 frames 不重要呢?我们从 instruction 执行顺序的角度来看待这个问题。

如果说,instruction 是按照“一个挨一个向下顺序执行”的,那么它们对应的 frames 就不重要;相应的,instruction 在执行过程时,它是从某个地方“跳转”过来的,那么对应的 frames 就重要。

为什么说 instruction 按照“一个挨一个向下顺序执行”的 frames 不重要呢?因为这些 instruction 对应的 frame 可以很容易的推导出来。 相反,如果当前的 instruction 是从某个地方跳转过来的,就必须要记录它执行之前的 frame 的情况,否则就没有办法计算它执行之后的 frame 的情况。当然,我们这里讲的只是大体的思路,而不是具体的判断细节。

经过压缩之后的 frames,就存放在 ClassFile 的 StackMapTable 结构中。

如何使用 visitFrame() 方法

如果想添加 frame 的相关信息,可以通过调用 MethodVisitor.visitFrame() 方法来实现:

                                                          ┌─── ClassReader
                                                          │
                                                          │
                                                          │                    ┌─── FieldVisitor
                                                          │                    │
                                  ┌─── asm.jar ───────────┼─── ClassVisitor ───┤
                                  │                       │                    │                     ┌─── visitLabel
                                  │                       │                    └─── MethodVisitor ───┤
                                  │                       │                                          └─── visitFrame
                                  │                       │                    ┌─── FieldWriter
                 ┌─── Core API ───┤                       └─── ClassWriter ────┤
                 │                │                                            └─── MethodWriter
                 │                │
                 │                ├─── asm-util.jar
ObjectWeb ASM ───┤                │
                 │                └─── asm-commons.jar
                 │
                 │                ┌─── asm-tree.jar
                 └─── Tree API ───┤
                                  └─── asm-analysis.jar

预期目标

public class HelloWorld {
    public void test(boolean flag) {
        if (flag) {
            System.out.println("value is true");
        }
        else {
            System.out.println("value is false");
        }
    }
}

编码实现

import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成 byte[] 内容
        byte[] bytes = dump();

        // (2) 保存 byte[] 到文件
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 创建 ClassWriter 对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);

        // (2) 调用 visitXxx() 方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(0, 0);
            mv1.visitEnd();
        }

        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(Z)V", null, null);
            Label elseLabel = new Label();
            Label returnLabel = new Label();

            // 第 1 段
            mv2.visitCode();
            mv2.visitVarInsn(ILOAD, 1);
            mv2.visitJumpInsn(IFEQ, elseLabel);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("value is true");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第 2 段
            mv2.visitLabel(elseLabel);
            mv2.visitFrame(F_SAME, 0, null, 0, null); // 调用 visitFrame() 方法
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("value is false");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            // 第 3 段
            mv2.visitLabel(returnLabel);
            mv2.visitFrame(F_SAME, 0, null, 0, null); // 调用 visitFrame() 方法
            mv2.visitInsn(RETURN);
            mv2.visitMaxs(2, 2);
            mv2.visitEnd();
        }

        cw.visitEnd();

        // (3) 调用 toByteArray() 方法
        return cw.toByteArray();
    }
}

在上面的代码中,我们创建 ClassWriter 对象时,使用了 ClassWriter.COMPUTE_MAXS 参数,这样 ASM 就会只计算 max locals 和 max stack 的值;在实现 test() 方法的时候,就需要明确的调用 MethodVisitor.visitFrame() 方法来添加相应的 frame 信息。

同时,我们也要注意到:MethodVisitor.visitLabel() 方法的调用在前,MethodVisitor.visitFrame() 方法的调用在后。因为 MethodVisitor.visitLabel() 方法是放置了一个潜在的跳转 label 目标,程序在跳转之后,就需要使用 MethodVisitor.visitFrame() 方法给出跳转之后 Frame 的具体情况。

验证结果

import java.lang.reflect.Method;

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sample.HelloWorld");
        Object obj = clazz.newInstance();

        Method method = clazz.getDeclaredMethod("test", boolean.class);
        method.invoke(obj, true);
        method.invoke(obj, false);
    }
}

不推荐使用 visitFrame() 方法

为什么我们不推荐调用 MethodVisitor.visitFrame() 方法呢?原因是计算 frame 本身就很麻烦,还容易出错。

我们在创建 ClassWriter 对象的时候,使用了 ClassWriter.COMPUTE_FRAMES 参数:

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

在使用了 ClassWriter.COMPUTE_FRAMES 参数之后,ASM 会忽略代码当中对于 MethodVisitor.visitFrame() 方法的调用,并且自动帮助我们计算 stack map frame 的具体内容。

总结

本文主要对 frame 进行了介绍,内容总结如下:

  • 第一点,在 ClassFile 结构中,StackMapTable 结构是如何得到的。
  • 第二点,不推荐使用 MethodVisitor.visitFrame() 方法,原因是 frame 的计算复杂,容易出错。我们可以在创建 ClassWriter 对象的时候,使用 ClassWriter.COMPUTE_FRAMES 参数,这样 ASM 就会帮助我们计算 frame 的值到底是多少。