用Java手写JVM第三章——解析class文件

文章目录

  • 代码目录
  • 一、class文件
  • 二、解析class文件
    • 1、读取数据
    • 2、整体结构
    • 3、魔术
    • 4、版本号
    • 5、类访问标志
    • 6、类和父类索引
    • 7、接口索引表
    • 8、字段和方法表
  • 三、解析常量池
    • 1、ConstantPool类
    • 2、ConstantInfo 接口
      • ①CONSTANT_Integer_info
      • ②CONSTANT_Float_info
      • ③ CONSTANT_Long_info
      • ④CONSTANT_Double_info
      • ⑤CONSTANT_Utf8_info
      • ⑥CONSTANT_String_info
      • ⑦CONSTANT_Class_info
      • ⑧CONSTANT_NameAndType_info
      • ⑨CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info
      • ⑩常量池小结
  • 四、解析属性表
    • 1、 AttributeInfo接口
      • ① Deprecated和Synthetic属性
      • ②SourceFile属性
      • ③ConstantValue属性
      • ④Code属性
      • ⑤Exceptions属性
      • ⑥LineNumberTable和LocalVariableTable属性
  • 五、测试

代码目录

ZYX-demo-jvm-03
├── pom.xml
└── src
    └── main
    │    └── java
    │        └── org.ZYX.demo.jvm
	│             ├── classfile
    │             │   ├── attributes  
    │             │   ├── constantpool 
    │             │   ├── ClassFile.java
    │             │   ├── ClassReader.java
    │             │   └── MemberInfo.java	
    │             ├── classpath
    │             │   ├── impl
    │             │   │   ├── CompositeEntry.java
    │             │   │   ├── DirEntry.java 
    │             │   │   ├── WildcardEntry.java 
    │             │   │   └── ZipEntry.java    
    │             │   ├── Classpath.java
    │             │   └── Entry.java    
    │             ├── Cmd.java
    │             └── Main.java
    └── test
         └── java
             └── org.ZYX.demo.test
                 └── HelloWorld.java

一、class文件

上一章我们加载了class文件。构成class文件的基本数据单位是字节,可以把整个class文件当成一个字节流来处理。稍大一些的数据由连续多个字节构成,这些数据在class文件中以大端(big-endian)方式存储。为了描述class文件格式,Java虚拟机规范定义了u1、u2和u4三种数据类型来表示1、2和4字节无符号整数。

相同类型的多条数据一般按表(table)的形式存储在class文件中。表由表头和表项(item)构成,表头是u2或u4整数。假设表头是n,后面就紧跟着n个表项数据。

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];
}

二、解析class文件

1、读取数据

解析class文件的第一步是从里面读取数据。虽然可以把class文件当成字节流来处理,但是直接操作字节很不方便,所以先定义一个类来帮助读取数据。这个类是classReader,代码如下:

public class ClassReader {

    private byte[] data;

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

    //读取u1类型
    public int readUint8() {
        byte[] val = readByte(1);
        return byte2int(val);
    }

    //读取u2类型
    public int readUint16() {
        byte[] val = readByte(2);
        return byte2int(val);
    }

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

    public int readUint32TInteger(){
        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();
    }
    
    //读取一个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;
    }

    public byte[] readBytes(int n) {
        return readByte(n);
    }
    
    /*readByte 方法在读取之后会将剩余的未读取字节复制到数组头部,
    这样每次只需要从第 0 个字节开始读就可以,而不用记录索引数据。
    */
    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;
    }

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

}

2、整体结构

有了classReader,我们来定义一个classFile类表示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);
    }
 }   

下面详细介绍class文件的各个部分。常量池和属性表比较复杂,放到后面单独讨论。

3、魔术

起标识作用

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

4、版本号

主版本号在J2SE 1.2之前是45,从1.2开始,每次有大的Java版本发布,都会加1。
用Java手写JVM第三章——解析class文件_第1张图片

    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();
    }

5、类访问标志

版本号之后是常量池,但是由于常量池比较复杂,所以放到3.3节介绍。常量池之后是类访问标志,这是一个16位的“bitmask”,指出class文件定义的是类还是接口,访问级别是public还是private,等等。本章只对class文件进行初步解析,并不做完整验证,所以只是读取类访问标志以备后用。第6章会详细讨论访问标志。

6、类和父类索引

类访问标志之后是两个u2类型的常量池索引,分别给出类名和父类名。class文件存储的类名类似完全限定名,但是把点换成了斜线,Java语言规范把这种名字叫作二进制名(binary names)。因为每个类都有名字,所以thisClass必须是有效的常量池索引。除java.lang.Object之外,其他类都有父类,所以superClass只在Object.class中是0,在其他class文件中必须是有效的常量池索引。

7、接口索引表

类和父类索引后面是接口索引表,表中存放的也是常量池索引,给出该类实现的所有接口的名字。

8、字段和方法表

接口索引表之后是字段表和方法表,分别存储字段和方法信息。字段和方法的基本结构大致相同,差别仅在于属性表。

为了避免重复代码,用一个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文件很大一部分数据,里面存放着各式各样的常量信息,包括数字和字符串常量、类和接口名、字段和方法名,等等。本节将详细介绍常量池和各种常量。

1、ConstantPool类

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

静态常量池在 class 文件中被组织成一个表,在手动解析静态常量池时,需要注意以下三点:
1、表头给出的常量池大小比常量池的实际大小大 1,如表头的值为 n,实际大小就是 n-1。
2、有效的常量池索引为 1~n-1,0 是无效索引。
3、CONSTANT_Long_info 和 CONSTANT_Double_info 占用常量池的两个位置。

public class ConstantPool {

    private ConstantInfo[] constantInfos;
    private final int siz;

    public ConstantPool(ClassReader reader) {
        //这个u2数字就是表头,代表常量池的大小;
        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;
            }
        }
    }
    //按照索引从常量池中寻找字段或方法的名字和描述符;
    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;
    }
    //方法通过下标从常量池查找类名,这个方法需要借助 getUtf8 方法,用来从常量池查找字符串;
    public String getClassName(int idx){
        ConstantClassInfo classInfo = (ConstantClassInfo) this.constantInfos[idx];
        return this.getUTF8(classInfo.nameIdx);
    }
    //从常量池查找UTF-8字符串
    public String getUTF8(int idx) {
        ConstantUtf8Info utf8Info = (ConstantUtf8Info) this.constantInfos[idx];
        return utf8Info == null ? "" : utf8Info.str();
    }

    public ConstantInfo[] getConstantInfos() {
        return constantInfos;
    }

    public void setConstantInfos(ConstantInfo[] constantInfos) {
        this.constantInfos = constantInfos;
    }

    public int getSiz() {
        return siz;
    }
}

2、ConstantInfo 接口

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

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

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;
}

接下来是它的三个方法:readInfo()、readConstantInfo()、newConstantInfo()

    //读取常量信息;
    void readInfo(ClassReader reader);

    int tag();
    
    //readConstantInfo()函数先读出tag值,然后调用newConstantInfo()函数创建具体的常量,
    //最后调用常量的readInfo()方法读取常量信息
    static ConstantInfo readConstantInfo(ClassReader reader, ConstantPool constantPool) {
        int tag = reader.readUint8();
        ConstantInfo constantInfo = newConstantInfo(tag, constantPool);
        constantInfo.readInfo(reader);
        return constantInfo;
    }

    static ConstantInfo newConstantInfo(int tag, ConstantPool constantPool) {
        switch (tag) {
            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_UTF8:
                return new ConstantUtf8Info();
            case CONSTANT_TAG_STRING:
                return new ConstantStringInfo(constantPool);
            case CONSTANT_TAG_CLASS:
                return new ConstantClassInfo(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_METHODTYPE:
                return new ConstantMethodTypeInfo();
            case CONSTANT_TAG_METHODHANDLE:
                return new ConstantMethodHandleInfo();
            case CONSTANT_TAG_INVOKEDYNAMIC:
                return new ConstantInvokeDynamicInfo();
            default:
                throw new ClassFormatError("constant pool tag");
        }
    }
}

后面小节将要详细介绍各种常量。

①CONSTANT_Integer_info

CONSTANT_Integer_info使用4字节存储整数常量。CONSTANT_Integer_info和后面将要介绍的其他三种数字常量无论是结构,还是实现,都非常相似。
其结构定义如下:

CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}

其代码如下:

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;
    }

    public int value(){
        return this.val;
    }

}

②CONSTANT_Float_info

CONSTANT_Float_info使用4字节存储IEEE754
单精度浮点数常量,结构如下:

CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
``
其代码如下:

```java
public class ConstantFloatInfo implements ConstantInfo {

    private float val;

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

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

    public float value(){
        return this.val;
    }

}

③ CONSTANT_Long_info

CONSTANT_Long_info使用8字节存储整数常量,其结构定义如下:

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

代码如下:

public class ConstantLongInfo implements ConstantInfo {

    private long val;

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

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

    public long value(){
        return this.val;
    }

}

④CONSTANT_Double_info

最后一个数字常量是CONSTANT_Double_info,使用8字节存储IEEE754双精度浮点数,结构如下:

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

代码如下:

public class ConstantDoubleInfo implements ConstantInfo {

    private double val;

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

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

    public double value(){
        return this.val;
    }

}

⑤CONSTANT_Utf8_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;
    }

    public String str(){
        return this.str;
    }

}

⑥CONSTANT_String_info

CONSTANT_String_info常量表示java.lang.String字面量,结构如下:

CONSTANT_String_info {
u1 tag;
u2 string_index;
}

CONSTANT_String_info本身并不存放字符串数据,只存了常量池索引,这个索引指向一个CONSTANT_Utf8_info常量。代码如下:

public class ConstantStringInfo implements ConstantInfo {

    private ConstantPool constantPool;
    private int strIdx;

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

    @Override
    //readInfo()方法读取常量池索引;
    public void readInfo(ClassReader reader) {
        this.strIdx = reader.readUint16();
    }

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

    //String()方法按索引从常量池中查找字符串;
    public String string(){
        return this.constantPool.getUTF8(this.strIdx);
    }


}

⑦CONSTANT_Class_info

CONSTANT_Class_info常量表示类或者接口的符号引用,结构如下:

CONSTANT_Class_info {
u1 tag;
u2 name_index;
}

可以看到,CONSTANT_String_info本身并不存放字符串数据,只存了常量池索引,这个索引指向一个CONSTANT_Utf8_info常量。

类和父类索引,以及接口表中的接口索引指向的都是CONSTANT_Class_info常量。

public class ConstantClassInfo implements ConstantInfo {

    public ConstantPool constantPool;
    public int nameIdx;

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

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

    public String name(){
        return this.constantPool.getUTF8(this.nameIdx);
    }

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

}

⑧CONSTANT_NameAndType_info

CONSTANT_NameAndType_info给出字段或方法的名称和描述符。CONSTANT_Class_infoCONSTANT_NameAndType_info加在一起可以唯一确定一个字段或者方法。其结构定义如下:

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

字段或方法名由name_index给出,字段或方法的描述符由descriptor_index给出。name_index和descriptor_index都是常量池索引,指向CONSTANT_Utf8_info常量。

Java虚拟机规范定义了一种简单的语法来描述字段和方法,可以根据下面的规则生成描述符
1、类型描述符
①基本类型 byte、short、char、int、long、float 和 double 的描述符都是单个字母,分别是 B、S、C、I、J、F 和 D(注意 Long 是 D)
②引用类型的描述符是L+类的完全限定名+分号。
③数组类型的描述符是[+数组元素类型描述符。
2、字段描述符就是字段类型的描述符。
3、方法描述符是(分号分隔的参数类型描述符)+返回值类型描述符,其中void返回值由单个字母V表示。

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

代码如下:

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_Methodref_info表示普通(非接口)方法符号引用,CONSTANT_InterfaceMethodref_info表示接口方法符号引用。这三种常量结构一模一样,为了节约篇幅,下面只给出CONSTANT_Fieldref_info的结构:

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

class_index和name_and_type_index都是常量池索引,分别指向CONSTANT_Class_infoCONSTANT_NameAndType_info常量。

由于三种常量的结构一致,我们定义一个父类ConstantMemberRefInfo,然后让这三种结构的类继承自这个父类,即可减少很多重复代码:

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。它们是Java SE7才添加到class文件中的,目的是支持新增的invokedynamic指令。我们不讨论invokedynamic指令,所以解析这三个常量的代码就不在这里介绍了。当然我已经在源码中实现了这三种常量。

四、解析属性表

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

1、 AttributeInfo接口

和常量池类似,各种属性表达的信息也各不相同,因此无法用统一的结构来定义。不同之处在于,常量是由Java虚拟机规范严格定义的,共有14种。但属性是可以扩展的,不同的虚拟机实现可以定义自己的属性类型。由于这个原因,Java虚拟机规范没有使用tag,而是使用属性名来区别不同的属性。属性数据放在属性名之后的u1表中,这样Java虚拟机实现就可以跳过自己无法识别的属性。属性的结构定义如下:

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

注意,属性表中存放的属性名实际上并不是编码后的字符串,而是常量池索引,指向常量池中的CONSTANT_Utf8_info常量。

与常量池类似,我们定义一个 AttributeInfo 接口,所有的属性实现类都要实现该接口。代码如下:

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;
    }
    
    /*读取单个属性;
    readAttribute()先读取属性名索引,根据它从常量池中找到属性名,
    然后读取属性长度,
    接着调用newAttributeInfo()函数创建具体的属性实例;
    */
    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;
    }
    
    //用于根据类型构造对应属性的实现类;
    static AttributeInfo newAttributeInfo(String attrName, int attrLen, ConstantPool constantPool) {
        switch (attrName) {
            case "BootstrapMethods":
                return new BootstrapMethodsAttribute();
            case "Code":
                return new CodeAttribute(constantPool);
            case "ConstantValue":
                return new ConstantValueAttribute();
            case "Deprecated":
                return new DeprecatedAttribute();
            case "EnclosingMethod":
                return new EnclosingMethodAttribute(constantPool);
            case "Exceptions":
                return new ExceptionsAttribute();
            case "InnerClasses":
                return new InnerClassesAttribute();
            case "LineNumberTable":
                return new LineNumberTableAttribute();
            case "LocalVariableTable":
                return new LocalVariableTableAttribute();
            case "LocalVariableTypeTable":
                return new LocalVariableTypeTableAttribute();
            // case "MethodParameters":
            // case "RuntimeInvisibleAnnotations":
            // case "RuntimeInvisibleParameterAnnotations":
            // case "RuntimeInvisibleTypeAnnotations":
            // case "RuntimeVisibleAnnotations":
            // case "RuntimeVisibleParameterAnnotations":
            // case "RuntimeVisibleTypeAnnotations":
            case "Signature":
                return new SignatureAttribute(constantPool);
            case "SourceFile":
                return new SourceFileAttribute(constantPool);
            // case "SourceDebugExtension":
            // case "StackMapTable":
            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;
    }

}

JVM 虚拟机规范预定义了 23 种属性,我们解析其中八种。

① Deprecated和Synthetic属性

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

Deprecated_attribute {
u2 attribute_name_index;
u4 attribute_length;        //由于这两个属性都没有信息,所以其 attribute_length 都为 0;
}

Synthetic_attribute {
u2 attribute_name_index;
u4 attribute_length;
}

Deprecated:
Deprecated属性用于指出类、接口、字段或方法已经不建议使用,

public class DeprecatedAttribute extends MarkerAttribute {


    @Override
    public void readInfo(ClassReader reader) {

    }

}

Synthetic:
Synthetic属性用来标记源文件中不存在、由编译器生成的类成员,引入Synthetic属性主要是为了支持嵌套类和嵌套接口。

public class SyntheticAttribute extends MarkerAttribute {


    @Override
    public void readInfo(ClassReader reader) {

    }
}

②SourceFile属性

SourceFile 是可选定长属性,只会出现在ClassFile结构中,用于指出源文件名。其结构定义如下:

SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;        //这个值必须是2;
u2 sourcefile_index;        //sourcefile_index是常量池索引,指向CONSTANT_Utf8_info常量;
}

其代码如下:

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;          //这个值必须是2;
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属性比较复杂,其结构定义如下:

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]; //属性表;
}

代码如下:

public class CodeAttribute implements AttributeInfo {

    private ConstantPool constantPool;
    private int maxStack;
    private int maxLocals;
    private byte[] data;
    private ExceptionTableEntry[] exceptionTable;
    private AttributeInfo[] attributes;

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

    @Override
    public void readInfo(ClassReader reader) {
        this.maxStack = reader.readUint16();
        this.maxLocals = reader.readUint16();
        int dataLength = (int) reader.readUint32();
        this.data = reader.readBytes(dataLength);
        this.exceptionTable = ExceptionTableEntry.readExceptionTable(reader);
        this.attributes = AttributeInfo.readAttributes(reader, this.constantPool);
    }

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

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

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

    public ExceptionTableEntry[] exceptionTable() {
        return this.exceptionTable;
    }

    public AttributeInfo[] attributes() {
        return this.attributes;
    }

    static class ExceptionTableEntry {

        private int startPC;
        private int endPC;
        private int handlerPC;
        private int catchType;

        ExceptionTableEntry(int startPC, int endPC, int handlerPC, int catchType) {
            this.startPC = startPC;
            this.endPC = endPC;
            this.handlerPC = handlerPC;
            this.catchType = catchType;
        }

        static ExceptionTableEntry[] readExceptionTable(ClassReader reader) {
            int exceptionTableLength = reader.readUint16();
            ExceptionTableEntry[] exceptionTable = new ExceptionTableEntry[exceptionTableLength];
            for (int i = 0; i < exceptionTableLength; i++) {
                exceptionTable[i] = new ExceptionTableEntry(reader.readUint16(), reader.readUint16(), reader.readUint16(), reader.readUint16());
            }
            return exceptionTable;
        }

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

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

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

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

    }

}

⑤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());
        }
    }

在第10章讨论异常处理时会详细讨论LineNumberTable属性。

五、测试

在第2章的测试中,把class文件加载到了内存中,并且把一堆看似杂乱无章的数字打印到了控制台。本节就来修改测试代码,把命令行工具临时打造成一个反编译工具。

main()函数不用变,修改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);
    }

loadClass()函数读取并解析class文件,代码如下:

 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()函数把class文件的一些重要信息打印出来,代码如下:

    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());
        }
    }

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

运行结果如下:

classpath:org.ZYX.demo.jvm.classpath.Classpath@7591083d classjava.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

感兴趣读者的可以自己跑一跑。如果读者想测试自己编写的类,记得要指定-classpath选项哦。

你可能感兴趣的:(手写JVM,java)