用Java手写jvm之尝试解析clazz文件内容

写在前面

源码

本文尝试来解析下class文件的内容,了解了class文件内容后,对我们提升java认知将会带来很大的帮助,有多大呢,不好说,总之很大很大,大到受不了😍😍😍。

1:前置知识

在开始正式解析class文件的结构之前,我们需要对class文件的结构有一个大概的了解,关于这部分内容,你可以参考我的另一篇文章class字节码文件结构是什么样子的?

另外还需要对class查找相关知识有一些了解,关于这部分内容你可以参考我的另一篇文章用Java手写jvm之实现查找class 。以下内容涉及到class查找的内容将不做单独讲解,源码部分也不再做单独说明,请知悉!

你还需要对常量池的结构有所了解,关于这部分内容可以参考我的另一篇文章class字节码文件常量池的结构以及都有哪些类型的数据

内容稍多,稳住!!!

2:正戏

在Java中万物皆对象,所以先来定义一个代表class文件的对象ClassFile:

java 复制代码
/**
 * class字节码映射类
 * 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]
 * }
 */
public class ClassFile {
    // minor version,小版本,一般是0
    private int minorVersion;
    // major version,主版本,8 52 其他依次+1和-1
    private int majorVersion;
    // 常量池
    private ConstantPool constantPool;
    // 访问修饰符,不同的int值代表不同的访问修饰符
    private int accessFlags;
    // 类信息值 常量池位置
    private int thisClassIdx;
    // 父类信息值 常量池位置
    private int supperClassIdx;
    // 父接口信息值 常量池位置
    private int[] interfaces;
    // 字段信息
    private MemberInfo[] fields;
    // 方法信息
    private MemberInfo[] methods;
    private AttributeInfo[] attributes;

    /**
     * @param classData class对应的字节码二进制内容
     */
    public ClassFile(byte[] classData) {
        ClassReader reader = new ClassReader(classData);
        this.readAndCheckMagic(reader);
        this.readAndCheckVersion(reader);
        this.constantPool = this.readConstantPool(reader);
//        this.accessFlags = reader.readUint16();
        /*
        访问修饰符(十六进制表示)
        ACC_PUBLIC       = 0x0001;
        ACC_PRIVATE      = 0x0002;
        ACC_PROTECTED    = 0x0004;
        ACC_STATIC       = 0x0008;
        ACC_FINAL        = 0x0010;
        ACC_SUPER        = 0x0020;
        ACC_SYNCHRONIZED = 0x0020;
        ACC_VOLATILE     = 0x0040;
        ACC_BRIDGE       = 0x0040;
        ACC_TRANSIENT    = 0x0080;
        ACC_VARARGS      = 0x0080;
        ACC_NATIVE       = 0x0100;
        ACC_INTERFACE    = 0x0200;
        ACC_ABSTRACT     = 0x0400;
        ACC_STRICT       = 0x0800;
        ACC_SYNTHETIC    = 0x1000;
        ACC_ANNOTATION   = 0x2000;
        ACC_ENUM         = 0x4000;
        */
        this.accessFlags = reader.readU2();
//        this.thisClassIdx = reader.readUint16();
        // 当前所属类 utf-8常量值 常量池数组索引地址
        this.thisClassIdx = reader.readU2();
//        this.supperClassIdx = reader.readUint16();
        // 父类
        this.supperClassIdx = reader.readU2();
        // 实现的接口们
        this.interfaces = reader.readUint16s();
        this.fields = MemberInfo.readMembers(reader, constantPool);
        this.methods = MemberInfo.readMembers(reader, constantPool);
        this.attributes = AttributeInfo.readAttributes(reader, constantPool);
    }
    // ...
}

在ClassFile中定义了魔法数字,版本号,常量值,字段信息等,这些数据都来自于class字节码文件,所以我们需要拥有解析class文件的能力,为此,需要再来定义一个class文件的读取类ClassReader:

java 复制代码
/**
 * 负责读取class文件信息的类
 */
public class ClassReader {
    /**
     * class文件对应的二进制数据
     */
    private byte[] clazzData;

    public ClassReader(byte[] clazzData) {
        this.clazzData = clazzData;
    }

    /**
     * jvm虚拟机规范,定义了u1,u2,u4三种数据类型,分别表示1字节无符号整数,2字节无符号整数,4字节无符号整数
     * class字节码文件中的各种结构,基本都是通过这3中数据类型来定义的,可参考如下结构体
     * ClassFile {
     *     u4 magic;
     *     u2 minor_version;
     *     u2 major_version;
     *     ...
     * }
     * 说明magic是u4类型,即magic是一个4字节的无符号整数,其他类似
     */
    private final static int JVM_DATA_TYPE_U1 = 1;
    private final static int JVM_DATA_TYPE_U2 = 2;
    private final static int JVM_DATA_TYPE_U4 = 4;

    // u1 转int 读1个字节
    public int readU1() {
//        byte[] val = readByte(1);
        byte[] val = readByte(JVM_DATA_TYPE_U1);
        return byte2int(val);
    }

    // u2 转int 读2个字节
    public int readU2() {
//        byte[] val = readByte(2);
        byte[] val = readByte(JVM_DATA_TYPE_U2);
        return byte2int(val);
    }

    // u4 转long 转long就可以高位填0,就可以作为正数显示
    public long readU4() {
//        byte[] val = readByte(4);
        byte[] val = readByte(JVM_DATA_TYPE_U4);
        String str_hex = new BigInteger(1, val).toString(16);
        return Long.parseLong(str_hex, 16);
    }

    // u4 转int
    public int readU4TInteger(){
        byte[] val = readByte(4);
        return new BigInteger(1, val).intValue();
    }

    public float readUint64TFloat() {
        byte[] val = readByte(8);
        return new BigInteger(1, val).floatValue();
    }

    public long readUint64TLong() {
        byte[] val = readByte(8);
        return new BigInteger(1, val).longValue();
    }

    public double readUint64TDouble() {
        byte[] val = readByte(8);
        return new BigInteger(1, val).doubleValue();
    }

    public int[] readUint16s() {
//        int n = this.readUint16();
        int n = this.readU2();
        int[] s = new int[n];
        for (int i = 0; i < n; i++) {
//            s[i] = this.readUint16();
            s[i] = this.readU2();
        }
        return s;
    }

    public byte[] readBytes(int n) {
        return readByte(n);
    }

    private byte[] readByte(int length) {
        byte[] copy = new byte[length];
        System.arraycopy(clazzData, 0, copy, 0, length);
        System.arraycopy(clazzData, length, clazzData, 0, clazzData.length - length);
        return copy;
    }

    private int byte2int(byte[] val) {
        String str_hex = new BigInteger(1, val).toString(16);
        return Integer.parseInt(str_hex, 16);
    }
}

在我们了解了class文件的结构之后就可以通过该工具类按照u1,u2,u4的长度来读取对应的信息,之后进行封装了。

2.1:常量池

为了表示常量池,我们定义类ConstantPool:

java 复制代码
public class ConstantPool {

    private ConstantInfo[] constantInfos;
    private final int siz;

    public ConstantPool(ClassReader reader) {
//        siz = reader.readUint16();
        siz = reader.readU2();
        constantInfos = new ConstantInfo[siz];
        for (int i = 1; i < siz; i++) {

            constantInfos[i] = ConstantInfo.readConstantInfo(reader, this);

            switch (constantInfos[i].tag()) {
                case ConstantInfo.CONSTANT_TAG_DOUBLE:
                case ConstantInfo.CONSTANT_TAG_LONG:
                    i++;
                    break;
            }
        }
    }
    // ...
}

ConstantInfo是代表常量类型的接口,如下:

java 复制代码
/**
 * 常量池接口
 */
public interface ConstantInfo {

    int CONSTANT_TAG_CLASS = 7;
    int CONSTANT_TAG_FIELDREF = 9;
    int CONSTANT_TAG_METHODREF = 10;
    int CONSTANT_TAG_INTERFACEMETHODREF = 11;
    int CONSTANT_TAG_STRING = 8;
    int CONSTANT_TAG_INTEGER = 3;
    int CONSTANT_TAG_FLOAT = 4;
    int CONSTANT_TAG_LONG = 5;
    int CONSTANT_TAG_DOUBLE = 6;
    int CONSTANT_TAG_NAMEANDTYPE = 12;
    int CONSTANT_TAG_UTF8 = 1;
    int CONSTANT_TAG_METHODHANDLE = 15;
    int CONSTANT_TAG_METHODTYPE = 16;
    int CONSTANT_TAG_INVOKEDYNAMIC = 18;
}

因为不同类型的常量取值方式都都不尽相同,所以针对每种常量定义对应的子类,如下:

具体看源码吧!

为了方便测试,我们先来编写一个要解析的类:

java 复制代码
public class TestConstantsPool {
    private static final String nameA = "jackkkk";
    private static final int ageB = 56;
    private static final Object objC = new Object();

    public static void main(String[] args) {
        System.out.println("jjjjjjjjjjjjjjjjj");
    }
}

接着如下代码测试:

java 复制代码
/**
 * program arguments:-Xthejrepath D:\programs\javas\java1.8/jre -Xthetargetclazz D:\test\itstack-demo-jvm-master\find-class-from-classpath\target\test-classes\com\dahuyou\find\clazz\test\ShowMeByteCode
 */
public class Main {

    public static void main(String[] args) {
        Cmd cmd = Cmd.parse(args);
        if (!cmd.ok || cmd.helpFlag) {
            System.out.println("Usage: <main class> [-options] class [args...]");
            return;
        }
        if (cmd.versionFlag) {
            //注意案例测试都是基于1.8,另外jdk1.9以后使用模块化没有rt.jar
            System.out.println("java version \"1.8.0\"");
            return;
        }
        startJVM(cmd);
    }

    private static void startJVM(Cmd cmd) {
        // 创建classpath
        Classpath cp = new Classpath(cmd.thejrepath, cmd.classpath);
//        System.out.printf("classpath:%s class:%s args:%s\n", cp, cmd.getMainClass(), cmd.getAppArgs());
        System.out.printf("classpath:%s parsed class:%s \n", cp, cmd.thetargetclazz);
        //获取className
//        String className = cmd.getMainClass().replace(".", "/");
        try {
//            byte[] classData = cp.readClass(className);
            /*byte[] classData = cp.readClass(cmd.thetargetclazz.replace(".", "/"));
            System.out.println(Arrays.toString(classData));
            System.out.println("classData:");
            for (byte b : classData) {
                //16进制输出
                System.out.print(String.format("%02x", b & 0xff) + " ");
            }*/
            String clazzName = cmd.thetargetclazz.replace(".", "/");
            // 创建className对应的ClassFile对象
            ClassFile classFile = loadClass(clazzName, cp);
            printClassInfo(classFile);

        } catch (Exception e) {
            System.out.println("Could not find or load main class " + cmd.getMainClass());
            e.printStackTrace();
        }
    }

    private static void printClassInfo(ClassFile cf) {
        System.out.println("version: " + cf.majorVersion() + "." + cf.minorVersion());
        System.out.println("constants count:" + cf.constantPool().getSiz());
        ConstantInfo[] constantInfoArr = cf.constantPool().getConstantInfos();
        System.out.println("-------常量池信息开始------");
        for (int i = 1; i < constantInfoArr.length; i++) {
            System.out.println("常量位置:" + i);
            ConstantInfo constantInfo = constantInfoArr[i];
            if (constantInfo != null) constantInfo.showInfo();
        }
        System.out.println("-------常量池信息结束------");
        /*System.out.format("access flags:0x%x\n", cf.accessFlags());
        System.out.println("this class:" + cf.className());
        System.out.println("super class:" + cf.superClassName());
        System.out.println("interfaces:" + Arrays.toString(cf.interfaceNames()));
        System.out.println("fields count:" + cf.fields().length);
        for (MemberInfo memberInfo : cf.fields()) {
            System.out.format("000000%s \t\t %s\n", memberInfo.name(), memberInfo.descriptor());
        }*/

        /*System.out.println("methods count: " + cf.methods().length);
        for (MemberInfo memberInfo : cf.methods()) {
            System.out.format("%s \t\t %s\n", memberInfo.name(), memberInfo.descriptor());
        }*/
    }

    /**
     * 生成class文件对象
     * @param clazzName
     * @param cp
     * @return
     */
    private static ClassFile loadClass(String clazzName, Classpath cp) {
        try {
            // 获取类class对应的byte数组
            byte[] classData = cp.readClass(clazzName);
            return new ClassFile(classData);
        } catch (Exception e) {
            System.out.println("无法加载到类: " + clazzName);
            return null;
        }
    }

}

然后要配置program arguments,如下:

-Xthejrepath D:\programs\javas\java1.8/jre -Xthetargetclazz D:\test\itstack-demo-jvm-master\try-to-parse-clazz-file\target\test-classes\com\dahuyou\test\TestConstantsPool

运行:

classpath:com.dahuyou.tryy.too.parse.clazz.file.classpath.Classpath@bebdb06 parsed class:D:\test\itstack-demo-jvm-master\try-to-parse-clazz-file\target\test-classes\com\dahuyou\test\TestConstantsPool 
version: 52.0
constants count:47
-------常量池信息开始------
常量位置:1
常量位置:2
tag is: 9, 字段符号引用信息是:{name=out, _type=Ljava/io/PrintStream;}
常量位置:3
tag 是:8, string 常量值是:jjjjjjjjjjjjjjjjj
常量位置:4
常量位置:5
tag 是:7, class 或 interface的符号引用是: java/lang/Object
常量位置:6
tag is: 9, 字段符号引用信息是:{name=objC, _type=Ljava/lang/Object;}
常量位置:7
tag 是:7, class 或 interface的符号引用是: com/dahuyou/test/TestConstantsPool
常量位置:8
tag 是:1, utf8值是:nameA
常量位置:9
tag 是:1, utf8值是:Ljava/lang/String;
常量位置:10
tag 是:1, utf8值是:ConstantValue
常量位置:11
tag 是:8, string 常量值是:jackkkk
常量位置:12
tag 是:1, utf8值是:ageB
常量位置:13
tag 是:1, utf8值是:I
常量位置:14
tag 是:3, integer值是:56
常量位置:15
tag 是:1, utf8值是:objC
常量位置:16
tag 是:1, utf8值是:Ljava/lang/Object;
常量位置:17
tag 是:1, utf8值是:<init>
常量位置:18
tag 是:1, utf8值是:()V
常量位置:19
tag 是:1, utf8值是:Code
常量位置:20
tag 是:1, utf8值是:LineNumberTable
常量位置:21
tag 是:1, utf8值是:LocalVariableTable
常量位置:22
tag 是:1, utf8值是:this
常量位置:23
tag 是:1, utf8值是:Lcom/dahuyou/test/TestConstantsPool;
常量位置:24
tag 是:1, utf8值是:main
常量位置:25
tag 是:1, utf8值是:([Ljava/lang/String;)V
常量位置:26
tag 是:1, utf8值是:args
常量位置:27
tag 是:1, utf8值是:[Ljava/lang/String;
常量位置:28
tag 是:1, utf8值是:<clinit>
常量位置:29
tag 是:1, utf8值是:SourceFile
常量位置:30
tag 是:1, utf8值是:TestConstantsPool.java
常量位置:31
tag 是:12, name and type 是:
常量位置:32
tag 是:7, class 或 interface的符号引用是: java/lang/System
常量位置:33
tag 是:12, name and type 是:
常量位置:34
tag 是:1, utf8值是:jjjjjjjjjjjjjjjjj
常量位置:35
tag 是:7, class 或 interface的符号引用是: java/io/PrintStream
常量位置:36
tag 是:12, name and type 是:
常量位置:37
tag 是:1, utf8值是:java/lang/Object
常量位置:38
tag 是:12, name and type 是:
常量位置:39
tag 是:1, utf8值是:com/dahuyou/test/TestConstantsPool
常量位置:40
tag 是:1, utf8值是:jackkkk
常量位置:41
tag 是:1, utf8值是:java/lang/System
常量位置:42
tag 是:1, utf8值是:out
常量位置:43
tag 是:1, utf8值是:Ljava/io/PrintStream;
常量位置:44
tag 是:1, utf8值是:java/io/PrintStream
常量位置:45
tag 是:1, utf8值是:println
常量位置:46
tag 是:1, utf8值是:(Ljava/lang/String;)V
-------常量池信息结束------

Process finished with exit code 0

可以看到变量名称,变量的值字面量,符号应用信息等都可以正常输出出来了。

2.2:字段信息和方法信息

java 复制代码
System.out.format("access flags:0x%x\n", cf.accessFlags());
System.out.println("this class:" + cf.className());
System.out.println("super class:" + cf.superClassName());
System.out.println("interfaces:" + Arrays.toString(cf.interfaceNames()));
System.out.println("fields count:" + cf.fields().length);
for (MemberInfo memberInfo : cf.fields()) {
    System.out.format("字段信息:%s \t\t %s\n", memberInfo.name(), memberInfo.descriptor());
}

System.out.println("methods count: " + cf.methods().length);
for (MemberInfo memberInfo : cf.methods()) {
    System.out.format("方法信息:%s \t\t %s\n", memberInfo.name(), memberInfo.descriptor());
}

输出:

classpath:com.dahuyou.tryy.too.parse.clazz.file.classpath.Classpath@bebdb06 parsed class:D:\test\itstack-demo-jvm-master\try-to-parse-clazz-file\target\test-classes\com\dahuyou\test\TestConstantsPool 
version: 52.0
constants count:47
access flags:0x21
this class:com/dahuyou/test/TestConstantsPool
super class:java/lang/Object
interfaces:[]
fields count:3
字段信息:nameA 		 Ljava/lang/String;
字段信息:ageB 		 I
字段信息:objC 		 Ljava/lang/Object;
methods count: 3
方法信息:<init> 		 ()V
方法信息:main 		 ([Ljava/lang/String;)V
方法信息:<clinit> 		 ()V

Process finished with exit code 0

写在后面

参考文章列表

JVM系列---J2SE8 Class文件

class字节码文件结构是什么样子的?

用Java手写jvm之实现查找class

class字节码文件常量池的结构以及都有哪些类型的数据

相关推荐
mmsx3 分钟前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康28 分钟前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘1 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意2 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
东阳马生架构2 小时前
JVM实战—1.Java代码的运行原理
jvm
FF在路上2 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.3 小时前
Mybatis-Plus
java·开发语言
不良人天码星3 小时前
lombok插件不生效
java·开发语言·intellij-idea