手写 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,如表头的值为 n,实际大小就是 n-1
- 有效的常量池索引为 1~n-1,0 是无效索引
CONSTANT_Long_info
和CONSTANT_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 规范定义了一种简单的方法来描述字段和方法,如下:
- 类型描述符
- 基本类型 byte、short、char、int、long、float 和 double 的描述符都是单个字母,分别是 B、S、C、I、J、F 和 D(注意 Long 是 D)
- 引用类型的描述符是
L类完全限定名;
- 数组类型的描述符是
[数组元素类型描述符
- 字段描述符,就是字段类型的描述符
- 方法描述符,
(分号分隔的参数类型描述符)返回值类型描述符
,其中 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