代码目录
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。
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_info和CONSTANT_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_info和CONSTANT_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 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
感兴趣读者的可以自己跑一跑。如果读者想测试自己编写的类,记得要指定-classpath选项哦。