《深入拆解Java虚拟机》学习笔记 Day09 - JVM常用工具

137 阅读5分钟

因为前面学习各种知识,发现很难理解,于是先跳到工具篇.

javac java文件编译为class文件

javac xxx.java

会生成xxx.class文件,在使用IDE时,这步骤会被IDE来执行.

public class Foo {
  private int tryBlock;
  private int catchBlock;
  private int finallyBlock;
  private int methodExit;

  public void test() {
    try {
      tryBlock = 0;
    } catch (Exception e) {
      catchBlock = 1;
    } finally {
      finallyBlock = 2;
    }
    methodExit = 3;
  }
}

javap:查阅 Java 字节码

javap xxx.class 其中.class可以省略

默认会打印非私有的方法和字段.

javap -p

默认情况下 javap 会打印所有非私有的字段和方法,当加了 -p 选项后,它还将打印私有的字段和方法。

javap -c

javap -v

image.png

image.png

基本信息

class 文件的版本号 minor version

class 文件的版本号(minor version: 0,major version: 52)

class 文件的版本号指的是编译生成该 class 文件时所用的 JRE 版本。由较新的 JRE 版本中的 javac 编译而成的 class 文件,不能在旧版本的 JRE 上跑,否则,会出现如下异常信息。(Java 8 对应的版本号为 52,Java 10 对应的版本号为 54。)

这里的版本号对应的是javac编译jdk的版本,上面的2个图,

major version

flags 访问权限

该类的访问权限(flags: (0x0021) ACC_PUBLIC, ACC_SUPER)

类的访问权限通常为 ACC_ 开头的常量。

this_class

该类(this_class: #8)的名字

super_class

父类(super_class: #2)的名字

interfaces

所实现接口(interfaces: 0)的数目.

fields

字段(fields: 4)的数目.

methods

方法(methods: 2)的数目.

attributes

属性(attributes: 1)的数目。

常量池 Constant pool

常量池中的每一项都有一个对应的索引,#1、#2这种.

常量池之间也会有引用

image.png

如果将它看成一个树结构的话,那么它的叶节点会是字符串常量

image.png

字段区域与方法区域

image.png

字段/方法的类型 descriptor

I

方法的数栈stack

代码区域一开始会声明该方法中的操作数栈(stack=xx)和局部变量数目(locals=xx)的最大值,以及该方法接收参数的个数(args_size=xx)。 这里局部变量指的是字节码中的局部变量,而非 Java 程序中的局部变量。

方法的字节码

每条字节码均标注了对应的偏移量(bytecode index,BCI),这是用来定位字节码的。比如说偏移量为 10 的跳转字节码 10: goto 35,将跳转至偏移量为 35 的字节码 35: aload_0。

image.png

异常表 Exception Table

也会使用偏移量来定位每个异常处理器所监控的范围(由 from 到 to 的代码区域),以及异常处理器的起始位置(target)。除此之外,它还会声明所捕获的异常类型(type)。其中,any 指代任意异常类型。

image.png

行数表LineNumberTable:

是 Java 源程序到字节码偏移量的映射。

如果你在编译时使用了 -g 参数(javac -g Foo.java),那么这里还将出现局部变量表(LocalVariableTable:),展示 Java 程序中每个局部变量的名字、类型以及作用域。

行数表和局部变量表均属于调试信息。Java 虚拟机并不要求 class 文件必备这些信息。

image.png

字节码操作数栈映射表 StackMapTable

image.png

amstools

OpenJDK的Code Tools项目

使用时,先把.class文件编译为jasm文件,然后再对jasm文件进行操作

$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm

$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm

JOL

JOL 可用于查阅 Java 虚拟机中对象的内存分布


$ java -jar /path/to/jol-cli-0.9-full.jar internals java.util.HashMap
$ java -jar /path/to/jol-cli-0.9-full.jar estimates java.util.HashMap

ASM

ASM下载地址

repository.ow2.org/nexus/conte…

ASM是一个字节码分析及修改框架。它被广泛应用于许多项目之中,例如 Groovy、Kotlin 的编译器,代码覆盖测试工具 Cobertura、JaCoCo,以及各式各样通过字节码注入实现的程序行为监控工具。甚至是 Java 8 中 Lambda 表达式的适配器类,也是借助 ASM 来动态生成的。

ASM 既可以生成新的 class 文件,也可以修改已有的 class 文件。前者相对比较简单一些。

ASM 甚至还提供了一个辅助类 ASMifier,它将接收一个 class 文件并且输出一段生成该 class 文件原始字节数组的代码。如果你想快速上手 ASM 的话,那么你可以借助 ASMifier 生成的代码来探索各个 API 的用法。

使用方法

echo '
public class Foo1   { 
    public static void main(String[] args) {
        boolean flag = trueif (flag) System.out.println("Hello, Java!");  
        if (flag == true) System.out.println("Hello, JVM!"); 
        }
}' > Foo1.java

javac Foo.java 这里的javac,是Java8版本的,高版本的好像有问题

java -cp /PATH/TO/asm-all-6.0_BETA.jar org.objectweb.asm.util.ASMifier Foo.class | tee FooDump.java

会生成如下代码

import java.util.*;
import org.objectweb.asm.*;
public class Foo1Dump implements Opcodes {

    public static byte[] dump () throws Exception {

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;

        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Foo1", null, "java/lang/Object", null);

        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv.visitInsn(RETURN);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
            mv.visitCode();
            mv.visitInsn(ICONST_1);
            mv.visitVarInsn(ISTORE, 1);
            mv.visitVarInsn(ILOAD, 1);
            Label l0 = new Label();
            mv.visitJumpInsn(IFEQ, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hello, Java!");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv.visitLabel(l0);
            mv.visitFrame(Opcodes.F_APPEND,1, new Object[] {Opcodes.INTEGER}, 0, null);
            mv.visitVarInsn(ILOAD, 1);
            mv.visitInsn(ICONST_1);
            Label l1 = new Label();
            mv.visitJumpInsn(IF_ICMPNE, l1);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hello, JVM!");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv.visitLabel(l1);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
            mv.visitInsn(RETURN);
            mv.visitMaxs(2, 2);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
}

image.png

可以看到,ASMifier 生成的代码中包含一个名为 FooDump 的类,其中定义了一个名为 dump 的方法。该方法将返回一个 byte 数组,其值为生成类的原始字节。

ClassReader与ClassWriter

ClassReader 将读取“Foo”类的原始字节,并且翻译成对应的访问请求。也就是说,在上面 ASMifier 生成的代码中的各个访问操作,现在都交给 ClassReader.accept 这一方法来发出了。那么,如何修改这个 class 文件的字节码呢?原理很简单,就是将 ClassReader 的访问请求发给另外一个访问者,再由这个访问者委派给 ClassWriter。 这样一来,新增操作可以通过在某一需要转发的请求后面附带新的请求来实现;删除操作可以通过不转发请求来实现;修改操作可以通过忽略原请求,新建并发出另外的请求来实现。

image.png

参考文章 :gk.link/a/11V4M