手写 JVM —— 2. 解析 class 文件

前言

补充下上一节的代码(老年人记性),到上一节为止的代码在https://github.com/CN-GuoZiyang/SpicyChickenJVM/tree/b4a2092f5af74de68441dbb16b49bc831fcb337d

上一节我们已经可以将任意类的字节码直接加载进虚拟机了,这一节我们来对这些看不懂的信息进行解析。

本节的代码位于 https://github.com/CN-GuoZiyang/SpicyChickenJVM/tree/5429210040d12d0f5ee67e8912ed94e906fe07ff

class 结构读取

上一节最后,我们将一个 class 文件的所有字节完整地读到了内存里,那么我们如何解析这些字节,就依赖 JVM 规范中定义的 class 文件的组织方式了。

JVM 规范定义了 u1、u2 和 u4 三种数据类型来分别表示 1、2 和 4 字节的无符号整数。如果有相同类型的多条数据,JVM 会按照表的形式存储,表分为表头和表项,一般表头就是一个 u2 或 u4 的数字,表示表项的个数,紧跟着表头的就是所有的表项了。

JVM 规范将 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];
}

我们来实现一个类 ClassReader 来方便地从字节数组中读取一定个数的字节,并且转换为不同的数据类型。我们用 int 来存储 u1 和 u2,用 long 存储 u4。

ClassReader 构造时传入了要被读取的字节数组,在读取时将从上一次读取的地方继续读取。

public class ClassReader {

    private byte[] data;

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

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

}    

readByte 方法在读取之后会将剩余的未读取字节复制到数组头部,这样每次只需要从第 0 个字节开始读就可以,而不用记录索引数据。

接着就可以包装一些方法来快速读取 u1、u2 和 u4。

    // read u1
    public int readUint8() {
        byte[] val = readByte(1);
        return byte2int(val);
    }

    // read u2
    public int readUint16() {
        byte[] val = readByte(2);
        return byte2int(val);
    }

    // read u4
    public long readUint32() {
        byte[] val = readByte(4);
        String str_hex = new BigInteger(1, val).toString(16);
        return Long.parseLong(str_hex, 16);
    }

    // 读取一个 uint16 数组,第一个元素指定了数组的长度
    public int[] readUint16s() {
        int n = this.readUint16();
        int[] s = new int[n];
        for (int i = 0; i < n; i++) {
            s[i] = this.readUint16();
        }
        return s;
    }

ClassFile

有了 ClassReader 辅助,我们就可以很方便地解析 class 文件了。我们首先来定义 class 文件结构:

public class ClassFile {

    private int minorVersion;
    private int majorVersion;
    private ConstantPool constantPool;
    private int accessFlags;
    private int thisClassIdx;
    private int supperClassIdx;
    private int[] interfaces;
    private MemberInfo[] fields;
    private MemberInfo[] methods;
    private AttributeInfo[] attributes;

    public ClassFile(byte[] classData) {
        ClassReader reader = new ClassReader(classData);
        this.readAndCheckMagic(reader);
        this.readAndCheckVersion(reader);
        this.constantPool = this.readConstantPool(reader);
        this.accessFlags = reader.readUint16();
        this.thisClassIdx = reader.readUint16();
        this.supperClassIdx = reader.readUint16();
        this.interfaces = reader.readUint16s();
        this.fields = MemberInfo.readMembers(reader, constantPool);
        this.methods = MemberInfo.readMembers(reader, constantPool);
        this.attributes = AttributeInfo.readAttributes(reader, constantPool);
    }

}

在构造时,就会将各个结构解析好存储在私有属性中。以解析 magicNumber 、minorVersion 和 majorVersion 为例:

    private void readAndCheckMagic(ClassReader reader) {
        long magic = reader.readUint32();
        if (magic != (0xCAFEBABE & 0x0FFFFFFFFL)) {
            throw new ClassFormatError("magic!");
        }
    }

    private void readAndCheckVersion(ClassReader reader) {
        this.minorVersion = reader.readUint16();
        this.majorVersion = reader.readUint16();
        switch (this.majorVersion) {
            case 45:
                return;
            case 46:
            case 47:
            case 48:
            case 49:
            case 50:
            case 51:
            case 52:
                if (this.minorVersion == 0)
                    return;
        }
        throw new UnsupportedClassVersionError();
    }

在解析获取数据时同时会检测数据合法性,以便及时抛出异常信息停止解析。

MemberInfo

因为字段和方法的结构基本一样,其差别仅仅在于属性表。所以我们定义一个 MemberInfo 类来存储字段和方法的基本信息。如下:

public class MemberInfo {

    private ConstantPool constantPool;
    private int accessFlags;
    private int nameIdx;
    private int descriptorIdx;
    private AttributeInfo[] attributes;

    private MemberInfo(ClassReader reader, ConstantPool constantPool) {
        this.constantPool = constantPool;
        this.accessFlags = reader.readUint16();
        this.nameIdx = reader.readUint16();
        this.descriptorIdx = reader.readUint16();
        this.attributes = AttributeInfo.readAttributes(reader, constantPool);
    }

}    

定义一个 readMembers 方法,用于读取字段表或方法表:

    static MemberInfo[] readMembers(ClassReader reader, ConstantPool constantPool) {
        int fieldCount = reader.readUint16();
        MemberInfo[] fields = new MemberInfo[fieldCount];
        for (int i = 0; i < fieldCount; i++) {
            fields[i] = new MemberInfo(reader, constantPool);
        }
        return fields;
    }

解析常量池

class 文件中的常量池,由于还没有被加载到 JVM 中,所以称之为 “静态常量池”。静态常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符

静态常量池在 class 文件中被组织成一个表,在手动解析静态常量池时,需要注意以下三点:

  1. 表头给出的常量池大小比常量池的实际大小大 1,如表头的值为 n,实际大小就是 n-1
  2. 有效的常量池索引为 1~n-1,0 是无效索引
  3. CONSTANT_Long_infoCONSTANT_Double_info 占用常量池的两个位置

我们新建一个类 ConstantPool,并在构造方法中就将常量池信息解析好:

    public ConstantPool(ClassReader reader) {
        siz = reader.readUint16();
        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;
            }
        }
    }

可以看到在最开头就读入了一个 u2 类型的数字,这个数字就是表头,代表常量池的大小。

getNameAndType 方法按照索引从常量池中寻找字段或方法的名字和描述符,如下:

    public Map<String, String> getNameAndType(int idx) {
        ConstantNameAndTypeInfo constantInfo = (ConstantNameAndTypeInfo) this.constantInfos[idx];
        Map<String, String> map = new HashMap<>();
        map.put("name", this.getUTF8(constantInfo.nameIdx));
        map.put("_type", this.getUTF8(constantInfo.descIdx));
        return map;
    }

getClassName 方法通过下标从常量池查找类名,这个方法需要借助 getUtf8 方法,用来从常量池查找字符串:

    public String getClassName(int idx){
        ConstantClassInfo classInfo = (ConstantClassInfo) this.constantInfos[idx];
        return this.getUTF8(classInfo.nameIdx);
    }

    public String getUTF8(int idx) {
        ConstantUtf8Info utf8Info = (ConstantUtf8Info) this.constantInfos[idx];
        return utf8Info == null ? "" : utf8Info.str();
    }

可以看到,找到代表类的 ConstantClassInfo 后,表示它的名字的 nameIdx 字段实际上又指向了常量池的另一个元素,所以还需要再次查找才能获得它的名字。

ConstantInfo 接口

由于每种常量的存放格式各不相同,所以我们定义 ConstantInfo 接口来实现一些通用的方法,具体的常量类型都需要实现这些方法。

尽管常量的格式不同,但是常量数据的第一个字节都是 tag,用于区分常量类型。我们在接口中直接定义好这些量:

public interface ConstantInfo {

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

}

我们定义一个方法 readInfo,用于具体的结构读取常量信息,这个方法留给具体的常量实现类来实现。静态方法 readConstantInfo 将从字节中读取 tag,并根据 tag 调用 newConstantInfo 方法创建具体的常量实现类,并调用其 readInfo 方法:

    void readInfo(ClassReader reader);

    static ConstantInfo readConstantInfo(ClassReader reader, ConstantPool constantPool) {
        int tag = reader.readUint8();
        ConstantInfo constantInfo = newConstantInfo(tag, constantPool);
        constantInfo.readInfo(reader);
        return constantInfo;
    }

newConstantInfo 方法根据 tag 值创建对应的实现类:

    static ConstantInfo newConstantInfo(int tag, ConstantPool constantPool) {
        switch (tag) {
            case CONSTANT_TAG_UTF8:
                return new ConstantUtf8Info();
            case CONSTANT_TAG_INTEGER:
                return new ConstantIntegerInfo();
            case CONSTANT_TAG_FLOAT:
                return new ConstantFloatInfo();
            case CONSTANT_TAG_LONG:
                return new ConstantLongInfo();
            case CONSTANT_TAG_DOUBLE:
                return new ConstantDoubleInfo();
            case CONSTANT_TAG_CLASS:
                return new ConstantClassInfo(constantPool);
            case CONSTANT_TAG_STRING:
                return new ConstantStringInfo(constantPool);
            case CONSTANT_TAG_FIELDREF:
                return new ConstantFieldRefInfo(constantPool);
            case CONSTANT_TAG_METHODREF:
                return new ConstantMethodRefInfo(constantPool);
            case CONSTANT_TAG_INTERFACEMETHODREF:
                return new ConstantInterfaceMethodRefInfo(constantPool);
            case CONSTANT_TAG_NAMEANDTYPE:
                return new ConstantNameAndTypeInfo();
            case CONSTANT_TAG_METHODHANDLE:
                return new ConstantMethodHandleInfo();
            case CONSTANT_TAG_METHODTYPE:
                return new ConstantMethodTypeInfo();
            case CONSTANT_TAG_INVOKEDYNAMIC:
                return new ConstantInvokeDynamicInfo();
            default:
                throw new ClassFormatError("constant pool tag");
        }
    }

数字常量实现类

Integer、Float、Long 和 Double 的常量类实现十分类似。所以以 Integer 为例。

Integer 类型的常量在常量池中的存储格式如下:

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}

除了第一个字节的 tag 外,它使用了 4 字节存储整数常量。

我们创建 Constan tIntegerInfo 类,来描述 Integer 常量,它需要实现 ConstantInfo 接口,并实现 readInfo 和 tag 方法:

public class ConstantIntegerInfo implements ConstantInfo{

    private int val;

    @Override
    public void readInfo(ClassReader reader) {
        this.val = reader.readUint32TInteger();
    }

    @Override
    public int tag() {
        return this.CONSTANT_TAG_INTEGER;
    }

}

CONSTANT_Integer_info 类型正好可以容纳一个 Java 的 int 类型常量,但是事实上比 int 更小的 boolean、byte、short 和 char 类型常量也都存储在 CONSTANT_Integer_info 中。

字符串常量实现类

字符串常量在常量池中使用 CONSTANT_Utf8_info 类型存储,表示一个 UTF-8 编码的字符串。这个结构定义如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

在底层,这个字符串使用字节数组表示,字节的个数就紧跟在 tag 后,使用一个 u2 类型的数字表示。于是,我们定义 ConstantUtf8Info 类,在 readInfo 方法中按顺序读取并转换成字符串。

public class ConstantUtf8Info implements ConstantInfo {

    private String str;

    @Override
    public void readInfo(ClassReader reader) {
        int length = reader.readUint16();
        byte[] bytes = reader.readBytes(length);
        this.str = new String(bytes);
    }

    @Override
    public int tag() {
        return this.CONSTANT_TAG_UTF8;
    }

}

类似的,有一个 CONSTANT_String_info 的常量表示 java.lang.String 字面量,其结构定义如下:

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

这个结构并不直接存储字符串数组,而是存储了一个常量池索引,这个索引会指向一个 CONSTANT_Utf8_info 常量。由于我们只需要实现这个结构,而不需要解索引,所以它的实现很简单:

public class ConstantStringInfo implements ConstantInfo {

    private ConstantPool constantPool;
    private int strIdx;

    public ConstantStringInfo(ConstantPool constantPool) {
        this.constantPool = constantPool;
    }

    @Override
    public void readInfo(ClassReader reader) {
        this.strIdx = reader.readUint16();
    }

    @Override
    public int tag() {
        return this.CONSTANT_TAG_STRING;
    }

}

类符号引用

CONSTANT_Class_info 常量表示对一个类或者接口的符号引用。ClassFile 中的类和超类索引,以及接口表中的接口索引都是指向这个类型的常量,其定义如下:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

这个常量仅仅记录了类或者接口的名字,其实现与 CONSTANT_String_info 几乎一致,name_index 也是指向了一个 CONSTANT_Utf8_info 常量。所以不再给出其具体实现。

字符或方法的名称和描述符

CONSTANT_NameAndType_info 给出一个字段或者方法的名称和描述符,它和 CONSTANT_Class_info 组合使用可以唯一确定一个字段或方法。其结构定义如下:

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

与前面一致,name_index 和 descriptor_index 都是常量池索引,指向 CONSTANT_Utf8_info 常量。name_index 就是字段或者方法的名称,而 descriptor_index则是描述符。JVM 规范定义了一种简单的方法来描述字段和方法,如下:

  1. 类型描述符
    1. 基本类型 byte、short、char、int、long、float 和 double 的描述符都是单个字母,分别是 B、S、C、I、J、F 和 D(注意 Long 是 D)
    2. 引用类型的描述符是 L类完全限定名;
    3. 数组类型的描述符是 [数组元素类型描述符
  2. 字段描述符,就是字段类型的描述符
  3. 方法描述符,(分号分隔的参数类型描述符)返回值类型描述符,其中 void 返回值由 V 表示

这就是为什么 Java 支持方法重载,因为描述一个方法不仅仅是它的方法名。注意,我们在 Java 语法层面无法定义同名字段,即使类型不同,但是在 class 文件的层面来看,其实是支持这一特性的。

我们来定义一个类 ConstantNameAndTypeInfo 来描述 CONSTANT_NameAndType_info 常量,它的实现很简单:

public class ConstantNameAndTypeInfo implements ConstantInfo {

    public int nameIdx;
    public int descIdx;

    @Override
    public void readInfo(ClassReader reader) {
        this.nameIdx = reader.readUint16();
        this.descIdx = reader.readUint16();
    }

    @Override
    public int tag() {
        return this.CONSTANT_TAG_NAMEANDTYPE;
    }

}

其他符号引用

CONSTANT_Fieldref_info 表示字段符号引用,CONSTANT_Methodref_info 表示普通(非接口)方法符号引用,CONSTANT_InterfaceMethodref_info 表示接口方法符号引用。它们的结构一模一样,所以我们只看 CONSTANT_Fieldref_info 的实现:

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

class_index 是常量池引用,指向了字段所在类的 CONSTANT_Class_info 常量,name_and_type_index 也是常量池引用,指向了这个方法的描述符 CONSTANT_NameAndType_info 常量。于是它的实现就很容易给出。由于三种常量的结构一致,我们定义一个父类,然后让这三种结构的类继承自这个父类,即可减少很多重复代码:

public class ConstantMemberRefInfo implements ConstantInfo {

    protected ConstantPool constantPool;
    protected int classIdx;
    private int nameAndTypeIdx;

    ConstantMemberRefInfo(ConstantPool constantPool) {
        this.constantPool = constantPool;
    }

    @Override
    public void readInfo(ClassReader reader) {
        this.classIdx = reader.readUint16();
        this.nameAndTypeIdx = reader.readUint16();
    }

    @Override
    public int tag() {
        return 0;
    }

    public String className() {
        return this.constantPool.getClassName(this.classIdx);
    }

    public Map<String, String> nameAndDescriptor() {
        return this.constantPool.getNameAndType(this.nameAndTypeIdx);
    }

}

于是 CONSTANT_Fieldref_info 的实现类如下:

public class ConstantFieldRefInfo extends ConstantMemberRefInfo  {

    public ConstantFieldRefInfo(ConstantPool constantPool) {
        super(constantPool);
    }

    @Override
    public int tag() {
        return this.CONSTANT_TAG_FIELDREF;
    }

}

剩下的还有 CONSTANT_MethodType_info、 CONSTANT_MethodHandle_info 和 CONSTANT_InvokeDynamic_info 这三种常量没有介绍,它们的用处是支持 invokedynamic 指令,在 Java 7 之后才被添加到 class 文件中。由于本教程实现的 JVM 并不打算支持动态方法调用,于是略去对这三种常量的介绍,感兴趣的同学可以去了解了解,当然我已经在源码中实现了这三种常量。

解析属性表

虽然常量池占据了 class 文件的大部分,但是常量池也仅仅是一个描述作用,一些更重要的信息:例如方法的实际字节码,还没有出现,这些信息都被存储在属性表中。当然属性表不仅仅存储字节码。

AttributeInfo 接口

与常量类型,属性也有不同的种类以表达不同的类型。与常量不同的是,常量是由 JVM 规范定义的,共有 14 种。但是属性并没有严格定义,不同的虚拟机可以实现不同的属性类型。基于这个原因,JVM 没有像常量一样,使用 tag 来标识类型,而是使用属性名来区分不同种类的属性。属性的数据会被放在属性名之后的 u1 表中:

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

JVM 可以根据实现情况选择性地读取属性。属性表的属性名也是一个常量池索引,指向了一个 CONSTANT_Utf8_info 常量。

与常量池类似,我们定义一个 AttributeInfo 接口,所有的属性实现类都要实现该接口,这个接口中有一个方法 readInfo,用来给具体属性实现其读取方式。readAttributes 方法读取所有的属性,readAttribute 则用于读取单个属性。

public interface AttributeInfo {

    void readInfo(ClassReader reader);

    static AttributeInfo[] readAttributes(ClassReader reader, ConstantPool constantPool) {
        int attributesCount = reader.readUint16();
        AttributeInfo[] attributes = new AttributeInfo[attributesCount];
        for (int i = 0; i < attributesCount; i++) {
            attributes[i] = readAttribute(reader, constantPool);
        }
        return attributes;
    }

    static AttributeInfo readAttribute(ClassReader reader, ConstantPool constantPool) {
        int attrNameIdx = reader.readUint16();
        String attrName = constantPool.getUTF8(attrNameIdx);
        int attrLen = reader.readUint32TInteger();
        AttributeInfo attrInfo = newAttributeInfo(attrName, attrLen, constantPool);
        attrInfo.readInfo(reader);
        return attrInfo;
    }

}

newAttribute 方法用于根据类型构造对应属性的实现类。JVM 虚拟机规范预定义了 23 种属性,我们先来解析其中八种:

    static AttributeInfo newAttributeInfo(String attrName, int attrLen, ConstantPool constantPool) {
        switch (attrName) {
            case "Code":
                return new CodeAttribute(constantPool);
            case "ConstantValue":
                return new ConstantValueAttribute();
            case "Deprecated":
                return new DeprecatedAttribute();
            case "Exceptions":
                return new ExceptionsAttribute();
            case "LineNumberTable":
                return new LineNumberTableAttribute();
            case "LocalVariableTable":
                return new LocalVariableTableAttribute();
            case "SourceFile":
                return new SourceFileAttribute(constantPool);
            case "Synthetic":
                return new SyntheticAttribute();
            default:
                return new UnparsedAttribute(attrName, attrLen);
        }

    }

最终未识别的属性直接生成 UnparsedAttribute,实现如下:

public class UnparsedAttribute implements AttributeInfo {

    private String name;
    private int length;
    private byte[] info;

    public UnparsedAttribute(String attrName, int attrLen) {
        this.name = attrName;
        this.length = attrLen;
    }

    @Override
    public void readInfo(ClassReader reader) {
        this.info = reader.readBytes(this.length);
    }

    public byte[] info(){
        return this.info;
    }

}

下面我们来看看这八种属性的具体实现。

标记属性

Deprecated 和 Synthetic 是最简单的两种属性,仅起标记作用,不包含任何数据。这两种标记可以出现在 ClassFile、field_info 和 method_info 中。它们的结构定义如下:

Deprecated_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
}
Synthetic_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
}

Deprecated 表示类、接口、字段或方法已经不建议使用。Synthetic 用来标记源文件中不存在、由编译器生成的类成员,引入Synthetic 属性主要是为了支持嵌套类和嵌套接口。

由于这两个属性都没有信息,所以其 attribute_length 都为 0。它们的实现中,readInfo 方法也是空方法。以 DeprecatedAttribute 为例:

public class DeprecatedAttribute extends MarkerAttribute {


    @Override
    public void readInfo(ClassReader reader) {

    }

}

SourceFile 属性

SourceFile 是一个可选的属性,出现在 ClassFile 结构中,用于指出这个类的源文件名。其定义如下:

SourceFile_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 sourcefile_index;
}

attribute_length 的值必须是2。sourcefile_index 是常量池索引,指向CONSTANT_Utf8_info 常量。

我们定义 SourceFileAttribute 类如下:

public class SourceFileAttribute implements AttributeInfo {

    private ConstantPool constantPool;
    private int sourceFileIdx;

    public SourceFileAttribute(ConstantPool constantPool) {
        this.constantPool = constantPool;
    }

    @Override
    public void readInfo(ClassReader reader) {
        this.sourceFileIdx = reader.readUint16();
    }

    public String fileName(){
        return this.constantPool.getUTF8(this.sourceFileIdx);
    }

}

ConstantValue 属性

ConstantValue 表示常量表达式的值,出现在 field_info 结构中。其定义如下:

ConstantValue_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
}

constantvalue_index 是常量池索引,但具体指向哪种常量因字段类型而异。例如字段是 long 类型时,就指向 CONSTANT_Long_info 常量。

对应实现如下:

public class ConstantValueAttribute implements AttributeInfo {

    private int constantValueIdx;

    @Override
    public void readInfo(ClassReader reader) {
        this.constantValueIdx = reader.readUint16();
    }

    public int constantValueIdx(){
        return this.constantValueIdx;
    }

}

Code 属性

Code 属性存放字节码等信息,只存在于 method_info 中。Code 属性比较复杂,定义如下:

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

max_stack 给出操作数栈的最大深度,max_locals 给出局部变量表大小。接着是字节码,存在 u1 表中。最后是异常处理表和属性表。

由于结构复杂,其实现代码较长,其实都大同小异,可以移步我的 Github 观看。

Exceptions 属性

Exceptions 是变长属性,记录方法抛出的异常表,其结构定义如下

Exceptions_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_exceptions;
    u2 exception_index_table[number_of_exceptions];
}

比较简单,直接上实现:

public class ExceptionsAttribute implements AttributeInfo {

    private int[] exceptionIndexTable;

    @Override
    public void readInfo(ClassReader reader) {
        this.exceptionIndexTable = reader.readUint16s();
    }

    public int[] exceptionIndexTable(){
        return this.exceptionIndexTable;
    }

}

LineNumberTable 和 LocalVariableTable

LineNumberTable 属性表存放方法的行号信息, LocalVariableTable 属性表中存放方法的局部变量信息。这两种属性和上面的 SourceFile 属性都属于调试信息,都不是运行时必需的。

这两个属性在结构上很像,所以以LineNumberTable 为例。结构定义如下:

LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;
        u2 line_number;
    } line_number_table[line_number_table_length];
}

其 readInfo 实现如下:

    @Override
    public void readInfo(ClassReader reader) {
        int lineNumberTableLength = reader.readUint16();
        this.lineNumberTable = new LineNumberTableEntry[lineNumberTableLength];
        for (int i = 0; i < lineNumberTableLength; i++) {
            lineNumberTable[i] = new LineNumberTableEntry(reader.readUint16(), reader.readUint16());
        }
    }

测试

我们将启动参数改为 -Xjre "C:\Program Files\Java\jdk1.8.0_271\jre" java.lang.String,来查看下 String 类中的字段和方法信息。

修改 startJVM 方法,在启动时生成主类的 ClassFile,并加载数据:

    private static void startJVM(Cmd cmd) {
        Classpath classpath = new Classpath(cmd.jre, cmd.classpath);
        System.out.printf("classpath:%s class:%s args:%s\n",
                classpath, cmd.getMainClass(), cmd.getArgs());
        //获取className
        String className = cmd.getMainClass().replace(".", "/");
        ClassFile classFile = loadClass(className, classpath);
        assert classFile != null;
        printClassInfo(classFile);
    }

    private static ClassFile loadClass(String className, Classpath classpath) {
        try {
            byte[] classData = classpath.readClass(className);
            return new ClassFile(classData);
        } catch (Exception e) {
            System.out.println("Could not find or load main class " + className);
            return null;
        }
    }

在最后输出类的相关信息,printClassInfo 方法如下:

    private static void printClassInfo(ClassFile cf) {
        System.out.println("version: " + cf.majorVersion() + "." + cf.minorVersion());
        System.out.println("constants count:" + cf.constantPool().getSiz());
        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:top.guoziyang.jvm.classpath.Classpath@736e9adb class:java.lang.String args:null
version: 52.0
constants count:540
access flags:0x31
this class:java/lang/String
super class:java/lang/Object
interfaces:[java/io/Serializable, java/lang/Comparable, java/lang/CharSequence]
fields count:5
value          [C
hash          I
serialVersionUID          J
serialPersistentFields          [Ljava/io/ObjectStreamField;
CASE_INSENSITIVE_ORDER          Ljava/util/Comparator;
methods count: 94
<init>          ()V
<init>          (Ljava/lang/String;)V
<init>          ([C)V
<init>          ([CII)V
<init>          ([III)V
<init>          ([BIII)V
<init>          ([BI)V
checkBounds          ([BII)V
<init>          ([BIILjava/lang/String;)V
<init>          ([BIILjava/nio/charset/Charset;)V
<init>          ([BLjava/lang/String;)V
<init>          ([BLjava/nio/charset/Charset;)V
<init>          ([BII)V
<init>          ([B)V
<init>          (Ljava/lang/StringBuffer;)V
<init>          (Ljava/lang/StringBuilder;)V
<init>          ([CZ)V
length          ()I
isEmpty          ()Z
charAt          (I)C
codePointAt          (I)I
codePointBefore          (I)I
codePointCount          (II)I
offsetByCodePoints          (II)I
getChars          ([CI)V
getChars          (II[CI)V
getBytes          (II[BI)V
getBytes          (Ljava/lang/String;)[B
getBytes          (Ljava/nio/charset/Charset;)[B
getBytes          ()[B
equals          (Ljava/lang/Object;)Z
contentEquals          (Ljava/lang/StringBuffer;)Z
nonSyncContentEquals          (Ljava/lang/AbstractStringBuilder;)Z
contentEquals          (Ljava/lang/CharSequence;)Z
equalsIgnoreCase          (Ljava/lang/String;)Z
compareTo          (Ljava/lang/String;)I
compareToIgnoreCase          (Ljava/lang/String;)I
regionMatches          (ILjava/lang/String;II)Z
regionMatches          (ZILjava/lang/String;II)Z
startsWith          (Ljava/lang/String;I)Z
startsWith          (Ljava/lang/String;)Z
endsWith          (Ljava/lang/String;)Z
hashCode          ()I
indexOf          (I)I
indexOf          (II)I
indexOfSupplementary          (II)I
lastIndexOf          (I)I
lastIndexOf          (II)I
lastIndexOfSupplementary          (II)I
indexOf          (Ljava/lang/String;)I
indexOf          (Ljava/lang/String;I)I
indexOf          ([CIILjava/lang/String;I)I
indexOf          ([CII[CIII)I
lastIndexOf          (Ljava/lang/String;)I
lastIndexOf          (Ljava/lang/String;I)I
lastIndexOf          ([CIILjava/lang/String;I)I
lastIndexOf          ([CII[CIII)I
substring          (I)Ljava/lang/String;
substring          (II)Ljava/lang/String;
subSequence          (II)Ljava/lang/CharSequence;
concat          (Ljava/lang/String;)Ljava/lang/String;
replace          (CC)Ljava/lang/String;
matches          (Ljava/lang/String;)Z
contains          (Ljava/lang/CharSequence;)Z
replaceFirst          (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
replaceAll          (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
replace          (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
split          (Ljava/lang/String;I)[Ljava/lang/String;
split          (Ljava/lang/String;)[Ljava/lang/String;
join          (Ljava/lang/CharSequence;[Ljava/lang/CharSequence;)Ljava/lang/String;
join          (Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;
toLowerCase          (Ljava/util/Locale;)Ljava/lang/String;
toLowerCase          ()Ljava/lang/String;
toUpperCase          (Ljava/util/Locale;)Ljava/lang/String;
toUpperCase          ()Ljava/lang/String;
trim          ()Ljava/lang/String;
toString          ()Ljava/lang/String;
toCharArray          ()[C
format          (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
format          (Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
valueOf          (Ljava/lang/Object;)Ljava/lang/String;
valueOf          ([C)Ljava/lang/String;
valueOf          ([CII)Ljava/lang/String;
copyValueOf          ([CII)Ljava/lang/String;
copyValueOf          ([C)Ljava/lang/String;
valueOf          (Z)Ljava/lang/String;
valueOf          (C)Ljava/lang/String;
valueOf          (I)Ljava/lang/String;
valueOf          (J)Ljava/lang/String;
valueOf          (F)Ljava/lang/String;
valueOf          (D)Ljava/lang/String;
intern          ()Ljava/lang/String;
compareTo          (Ljava/lang/Object;)I
<clinit>          ()V
全部评论
感谢参与【创作者计划2期·技术干货场】!欢迎更多牛油来写干货,瓜分总计20000元奖励!!技术干货场活动链接:https://www.nowcoder.com/link/czz2jsghtlq(参与奖马克杯将于每周五结算,敬请期待~)
点赞 回复 分享
发布于 2021-03-08 16:14
我还以为现在面试硬核到都要手写JVM了。。
点赞 回复 分享
发布于 2021-03-12 10:55

相关推荐

听说改名字就能收到offer哈:Radis写错了兄弟
点赞 评论 收藏
分享
评论
4
6
分享
牛客网
牛客企业服务