跳至主要內容

解析Class文件

Mr.Hope...大约 31 分钟

解析Class文件

1.1 class文件介绍

详细class文件分析open in new window

作为类/接口信息的载体,每一个class文件都完整的定义了一个类,为了使Java程序可以实现“编写一次,处处运行”,java虚拟机对class文件的格式进行了严格的规范。

但是对于从哪里加载class文件,给予了高度自由空间:第三节中说过,可以从文件系统读取jar/zip文件中的class文件,除此之外,也可以从网络下载,甚至是直接在运行中生成class文件

构成class文件的基本数据单位是字节,可以把整个class文件当 成一个字节流来处理。稍大一些的数据由连续多个字节构成,这些数据在class文件中以大端(big-endian)方式存储。

为了描述class文件格式,Java虚拟机规范定义了u1u2u4三种数据类型来表示1、 2和4字节无符号整数,分别对应Go语言的uint8uint16uint32类型。

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

Java虚拟机规范使用一种类似C语言的结构体语法来描述class 文件格式。整个class文件被描述为一个ClassFile结构,代码如下:

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

!表示大小不定。

1.2解析class文件

Go语言内置了丰富的数据类型,非常适合处理class文件。

如下为Go和Java语言基本数据类型对照关系:

Go语言类型Java语言类型说明
int8byte8比特有符号整数
uint8(别名byte)N/A8比特无符号整数
int16short16比特有符号整数
uint16char16比特无符号整数
int32(别名rune)int32比特有符号整数
uint32N/A32比特无符号整数
int64long64比特有符号整数
uint64N/A64比特无符号整数
float32float32比特IEEE-754浮点数
float64double64比特IEEE-754浮点数
1.2.1读取数据

解析class文件的第一步是从里面读取数据。虽然可以把class文件当成字节流来处理,但是直接操作字节很不方便,所以先定义一个结构体ClassReader来帮助读取数据,创建class_reader.go。

package classfile
import "encoding/binary"
type ClassReader struct {
	data []byte
}
func (self *ClassReader) readUint8() uint8 {...} // u1
func (self *ClassReader) readUint16() uint16 {...} // u2
func (self *ClassReader) readUint32() uint32 {...} // u4
func (self *ClassReader) readUint64() uint64 {...}
func (self *ClassReader) readUint16s() []uint16 {...}
func (self *ClassReader) readBytes(length uint32) []byte {...}

ClassReader只是[]byte类型的包装而已。readUint8()读取u1类型数据。

ClassReader并没有使用索引记录数据位置,而是使用Go 语言的reslice语法跳过已经读取的数据

实现代码如下:

// u1
func (self *ClassReader) readUint8() uint8 {
    val := self.data[0]
    self.data = self.data[1:]
    return val
}

// u2
func (self *ClassReader) readUint16() uint16 {
    val := binary.BigEndian.Uint16(self.data)
    self.data = self.data[2:]
    return val
}

// u4
func (self *ClassReader) readUint32() uint32 {
    val := binary.BigEndian.Uint32(self.data)
    self.data = self.data[4:]
    return val
}

func (self *ClassReader) readUint64() uint64 {
    val := binary.BigEndian.Uint64(self.data)
    self.data = self.data[8:]
    return val
}

func (self *ClassReader) readUint16s() []uint16 {
    n := self.readUint16()
    s := make([]uint16, n)
    for i := range s {
       s[i] = self.readUint16()
    }
    return s
}

func (self *ClassReader) readBytes(n uint32) []byte {
    bytes := self.data[:n]
    self.data = self.data[n:]
    return bytes
}

Go标准库encoding/binary包中定义了一个变量BigEndian,可以从[]byte中解码多字节数据。

1.2.2解析整体结构

有了ClassReader,可以开始解析class文件了。创建class_file.go文件,在其中定义ClassFile结构体,与1.1中的class文件中字段对应。

package classfile
import "fmt"

type ClassFile struct {
    //magic uint32
    minorVersion uint16
    majorVersion uint16
    constantPool ConstantPool
    accessFlags uint16
    thisClass uint16
    superClass uint16
    interfaces []uint16
    fields []*MemberInfo
    methods []*MemberInfo
    attributes []AttributeInfo
}

在class_file.go文件中实现一系列函数和方法。

func Parse(classData []byte) (cf *ClassFile, err error) {...}
func (self *ClassFile) read(reader *ClassReader) {...}
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {...}
func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {...}
func (self *ClassFile) MinorVersion() uint16 {...} // getter
func (self *ClassFile) MajorVersion() uint16 {...} // getter
func (self *ClassFile) ConstantPool() ConstantPool {...} // getter
func (self *ClassFile) AccessFlags() uint16 {...} // getter
func (self *ClassFile) Fields() []*MemberInfo {...} // getter
func (self *ClassFile) Methods() []*MemberInfo {...} // getter
func (self *ClassFile) ClassName() string {...}
func (self *ClassFile) SuperClassName() string {...}
func (self *ClassFile) InterfaceNames() []string {...}

相比Java语言,Go的访问控制非常简单:只有公开和私有两种。

所有首字母大写的类型、结构体、字段、变量、函数、方法等都是公开的,可供其他包使用。 首字母小写则是私有的,只能在包内部使用。

解析[]byte

Parse()函数把[]byte解析成ClassFile结构体。

func Parse(classData []byte) (cf *ClassFile, err error) {
    defer func() {
       //尝试捕获 panic,并将其存储在变量 r 中。如果没有发生 panic,r 将为 nil。
       if r := recover(); r != nil {
          var ok bool
          //判断 r 是否是一个 error 类型
          err, ok = r.(error)
          if !ok {
             err = fmt.Errorf("%v", r)
          }
       }
    }()

    cr := &ClassReader{classData}
    cf = &ClassFile{}
    cf.read(cr)
    return
}
顺序解析

read() 方法依次调用其他方法解析class文件,顺序一定要保证正确,与class文件相对应。

func (self *ClassFile) read(reader *ClassReader) {
  	//读取并检查类文件的魔数。 
    self.readAndCheckMagic(reader)
	//读取并检查类文件的版本号。
    self.readAndCheckVersion(reader)
    //解析常量池,常量池类还没写
    self.constantPool = readConstantPool(reader)
    //读取类的访问标志
    self.accessFlags = reader.readUint16()
    //读取指向当前类在常量池中的索引
    self.thisClass = reader.readUint16()
    //父类在常量池中的索引
    self.superClass = reader.readUint16()
    //读取接口表的数据
    self.interfaces = reader.readUint16s()
    //读取类的字段信息
    self.fields = readMembers(reader, self.constantPool)
    //读取类的方法信息
    self.methods = readMembers(reader, self.constantPool)
    //读取类的属性信息(类级别的注解、源码文件等)
    self.attributes = readAttributes(reader, self.constantPool)
}
  1. self.readAndCheckMagic(reader): 这是一个 ClassFile 结构的方法,用于读取并检查类文件的魔数。魔数是类文件的标识符,用于确定文件是否为有效的类文件。
  2. self.readAndCheckVersion(reader): 这个方法用于读取并检查类文件的版本号。Java类文件具有版本号,标识了它们的Java编译器版本。这里会对版本号进行检查。
  3. self.constantPool = readConstantPool(reader): 这一行代码调用 readConstantPool 函数来读取常量池部分的数据,并将其存储在 ClassFile 结构的 constantPool 字段中。常量池是一个包含各种常量信息的表格,用于支持类文件中的各种符号引用。
  4. self.accessFlags = reader.readUint16(): 这一行代码读取类的访问标志,它标识类的访问权限,例如 publicprivate 等。
  5. self.thisClass = reader.readUint16(): 这行代码读取指向当前类在常量池中的索引,表示当前类的类名。
  6. self.superClass = reader.readUint16(): 这行代码读取指向父类在常量池中的索引,表示当前类的父类名。
  7. self.interfaces = reader.readUint16s(): 这行代码读取接口表的数据,表示当前类实现的接口。
  8. self.fields = readMembers(reader, self.constantPool): 这行代码调用 readMembers 函数,以读取类的字段信息,并将它们存储在 fields 字段中。字段包括类的成员变量。
  9. self.methods = readMembers(reader, self.constantPool): 这行代码类似于上一行,但它读取类的方法信息,并将它们存储在 methods 字段中。
  10. self.attributes = readAttributes(reader, self.constantPool): 最后,这行代码调用 readAttributes 函数,以读取类的属性信息,并将它们存储在 attributes 字段中。属性包括类级别的注解、源码文件等信息。

以下均为类似于Java的getter方法,以后将不再赘述。

func (self *ClassFile) MinorVersion() uint16 {
	return self.minorVersion
}
func (self *ClassFile) MajorVersion() uint16 {
	return self.majorVersion
}
func (self *ClassFile) ConstantPool() ConstantPool {
	return self.constantPool
}
func (self *ClassFile) AccessFlags() uint16 {
	return self.accessFlags
}
func (self *ClassFile) Fields() []*MemberInfo {
	return self.fields
}
func (self *ClassFile) Methods() []*MemberInfo {
	return self.methods
}

ClassName从常量池中获取,SuperClassName同理,常量池还未实现。

所有类的超类(父类),Object是java中唯一没有父类的类,一个类可以不是Object的直接子类,但一定是继承于Object并拓展于Object。

func (self *ClassFile) ClassName() string {
    return self.constantPool.getClassName(self.thisClass)
}
func (self *ClassFile) SuperClassName() string {
	if self.superClass > 0 {
		return self.constantPool.getClassName(self.superClass)
	}
    //Object类
	return ""
}

Java的类是单继承,多实现的,因此获取接口应该使用循环,也从常量池中获取。

func (self *ClassFile) InterfaceNames() []string {
    interfaceNames := make([]string, len(self.interfaces))
    for i, cpIndex := range self.interfaces {
       interfaceNames[i] = self.constantPool.getClassName(cpIndex)
    }
    return interfaceNames
}
解析魔数

很多文件格式都会规定满足该格式的文件必须以某几个固定字节开头,这几个字节主要起标识作用,叫作魔数(magic number)

  • PDF文件以4字节“%PDF”(0x25、0x50、0x44、0x46)开头
  • ZIP 文件以2字节“PK”(0x50、0x4B)开头
  • class文件的魔数 是“0xCAFEBABE” 。

因此readAndCheckMagic()方法的代码如下。

func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {
    magic := reader.readUint32()
    if magic != 0xCAFEBABE {
       panic("java.lang.ClassFormatError: magic!")
    }
}

Java虚拟机规范规定,如果加载的class文件不符合要求的格式,Java虚拟机实现就抛出java.lang.ClassFormatError异常。

但是因为我们才刚刚开始编写虚拟机,还无法抛出异常,所以暂时先调用 panic()方法终止程序执行。

版本号

解析版本号

魔数之后是class文件的次版本号和主版本号,都是u2类型

假设某class文件的主版本号是M,次版本号是m,那么完整的版本号 可以表示成M.m的形式。 次版本号只在J2SE 1.2之前用过,从1.2 开始基本上就没什么用了(都是0)。 主版本号在J2SE 1.2之前是45, 从1.2开始,每次有大的Java版本发布,都会加1。

Java 版本类文件版本号
Java 1.145.3
Java 1.246.0
Java 1.347.0
Java 1.448.0
Java 549.0
Java 650.0
Java 751.0
Java 852.0

特定的Java虚拟机实现只能支持版本号在某个范围内的class文 件。 Oracle的实现是完全向后兼容的,比如Java SE 8支持版本号为 45.0~52.0的class文件。

如果版本号不在支持的范围内,Java虚拟机 实现就抛出java.lang.UnsupportedClassVersionError异常。参考 Java 8,支持版本号为45.0~52.0的class文件。如果遇到其他版本号, 调用panic()方法终止程序执行。 如下为检查版本号代码:

func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {
    self.minorVersion = reader.readUint16()
    self.majorVersion = reader.readUint16()
    switch self.majorVersion {
    case 45:
       return
    case 46, 47, 48, 49, 50, 51, 52:
       if self.minorVersion == 0 {
          return
       }
    }
    panic("java.lang.UnsupportedClassVersionError!")
}
解析类访问标识

版本号之后是常量池,但是由于常量池比较复杂,所以放到1.3 节介绍。

常量池之后是类访问标志,这是一个16位的bitmask,指出class文件定义的是类还是接口,访问级别是public还是private,等等。

本章只对class文件进行初步解析,并不做完整验证,所以只是读取类访问标志以备后用。

ClassFileTest的类访问标志为:0X21:

解析类和父类索引

类访问标志之后是两个u2类型的常量池索引,分别给出类名和超类名。

class文件存储的类名类似完全限定名,但是把点换成了 斜线,Java语言规范把这种名字叫作二进制名binary names

因为每个类都有名字,所以thisClass必须是有效的常量池索引。 除 java.lang.Object之外,其他类都有超类,所以superClass只在 Object.class中是0,在其他class文件中必须是有效的常量池索引。如下,ClassFileTest的类索引是5,超类索引是6。

解析接口索引表

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

解析字段和方法表

接口索引表之后是字段表和方法表,分别存储字段和方法信息。

字段和方法的基本结构大致相同,差别仅在于属性表。 下面是 Java虚拟机规范给出的字段结构定义

field_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

和类一样,字段和方法也有自己的访问标志。访问标志之后是一个常量池索引,给出字段名或方法名,然后又是一个常量池索引,给出字段或方法的描述符,最后是属性表。

为了避免重复代 码,用一个结构体统一表示字段和方法。

package classfile
type MemberInfo struct {
    cp ConstantPool
    accessFlags uint16
    nameIndex uint16
    descriptorIndex uint16
    attributes []AttributeInfo
}
func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {...}
func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {...}
func (self *MemberInfo) AccessFlags() uint16 {...} // getter
func (self *MemberInfo) Name() string {...}
func (self *MemberInfo) Descriptor() string {...}

cp字段保存常量池指针,后面会用到它。readMembers()读取字段表或方法表,代码如下:

func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {
    memberCount := reader.readUint16()
    members := make([]*MemberInfo, memberCount)
    for i := range members {
       members[i] = readMember(reader, cp)
    }
    return members
}

readMember()函数读取字段或方法数据。

func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {
    return &MemberInfo{
       cp:              cp,
       accessFlags:     reader.readUint16(),
       nameIndex:       reader.readUint16(),
       descriptorIndex: reader.readUint16(),
       attributes:      readAttributes(reader, cp),
    }
}

Name()从常量 池查找字段或方法名,Descriptor()从常量池查找字段或方法描述 符

func (self *MemberInfo) Name() string {
    return self.cp.getUtf8(self.nameIndex)
}
func (self *MemberInfo) Descriptor() string {
    return self.cp.getUtf8(self.descriptorIndex)
}
1.2.3解析常量池

常量池占据了class文件很大一部分数据,里面存放着各式各样的常量信息,包括数字和字符串常量、类和接口名、字段和方法名,等等

创建constant_pool.go文件,里面定义 ConstantPool类型

package classfile

type ConstantPool []ConstantInfo
func readConstantPool(reader *ClassReader) ConstantPool {...}
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {...}
func (self ConstantPool) getNameAndType(index uint16) (string, string) {...}
func (self ConstantPool) getClassName(index uint16) string {...}
func (self ConstantPool) getUtf8(index uint16) string {...}

常量池实际上也是一个表,但是有三点需要特别注意。

表头给出的常量池大小比实际大1。假设表头给出的值是n,那么常量池的实际大小是n–1。

有效的常量池索引是1~n–1。0是无效索引,表示不指向任何常量。

CONSTANT_Long_info CONSTANT_Double_info各占两个位置。也就是说,如果常量池中存在这两种常量,实际的常量数量比n–1还要少,而且1~n–1的某些 数也会变成无效索引。

常量池由readConstantPool()函数读取,代码如下:

func readConstantPool(reader *ClassReader) ConstantPool {
    cpCount := int(reader.readUint16())
    cp := make([]ConstantInfo, cpCount)

    // 索引从1开始
    for i := 1; i < cpCount; i++ {
       cp[i] = readConstantInfo(reader, cp)
       switch cp[i].(type) {
       //占两个位置
       case *ConstantLongInfo, *ConstantDoubleInfo:
          i++
       }
    }

    return cp
}

getConstantInfo()方法按索引查找常量

func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {
    if cpInfo := self[index]; cpInfo != nil {
       return cpInfo
    }
    panic(fmt.Errorf("Invalid constant pool index: %v!", index))
}

getNameAndType()方法从常量池查找字段或方法的名字和描述符

func (self ConstantPool) getNameAndType(index uint16) (string, string) {
    ntInfo := self.getConstantInfo(index).(*ConstantNameAndTypeInfo)
    name := self.getUtf8(ntInfo.nameIndex)
    _type := self.getUtf8(ntInfo.descriptorIndex)
    return name, _type
}

getClassName()方法从常量池查找类名

func (self ConstantPool) getClassName(index uint16) string {
    classInfo := self.getConstantInfo(index).(*ConstantClassInfo)
    return self.getUtf8(classInfo.nameIndex)
}

getUtf8()方法从常量池查找UTF-8字符串

func (self ConstantPool) getUtf8(index uint16) string {
    utf8Info := self.getConstantInfo(index).(*ConstantUtf8Info)
    return utf8Info.str
}
ConstPool接口

由于常量池中存放的信息各不相同,所以每种常量的格式也不同。 常量数据的第一字节是tag,用来区分常量类型。

下面是Java 虚拟机规范给出的常量结构

cp_info {
    u1 tag;
    u1 info[];
}

Java虚拟机规范一共定义了14种常量。创建constant_info.go文件,在其中定义tag常量值,代码如下:

package classfile

// Constant pool tags
const (
    CONSTANT_Class              = 7
    CONSTANT_Fieldref           = 9
    CONSTANT_Methodref          = 10
    CONSTANT_InterfaceMethodref = 11
    CONSTANT_String             = 8
    CONSTANT_Integer            = 3
    CONSTANT_Float              = 4
    CONSTANT_Long               = 5
    CONSTANT_Double             = 6
    CONSTANT_NameAndType        = 12
    CONSTANT_Utf8               = 1
    CONSTANT_MethodHandle       = 15
    CONSTANT_MethodType         = 16
    CONSTANT_InvokeDynamic      = 18
)

定义ConstantInfo接口来表示常量信息

type ConstantInfo interface {
	readInfo(reader *ClassReader)
}
//读取常量信息
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {...}
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {...}

readInfo()方法读取常量信息,需要由具体的常量结构体实现。 readConstantInfo()函数先读出tag值,然后调用newConstantInfo()函数创建具体的常量,最后调用常量的readInfo()方法读取常量信息, 代码如下:

func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {
    tag := reader.readUint8()
    c := newConstantInfo(tag, cp)
    c.readInfo(reader)
    return c
}

newConstantInfo()根据tag值创建具体的常量,代码如下:

func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {
    switch tag {
    case CONSTANT_Integer:
       return &ConstantIntegerInfo{}
    case CONSTANT_Float:
       return &ConstantFloatInfo{}
    case CONSTANT_Long:
       return &ConstantLongInfo{}
    case CONSTANT_Double:
       return &ConstantDoubleInfo{}
    case CONSTANT_Utf8:
       return &ConstantUtf8Info{}
    case CONSTANT_String:
       return &ConstantStringInfo{cp: cp}
    case CONSTANT_Class:
       return &ConstantClassInfo{cp: cp}
    case CONSTANT_Fieldref:
       return &ConstantFieldrefInfo{ConstantMemberrefInfo{cp: cp}}
    case CONSTANT_Methodref:
       return &ConstantMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
    case CONSTANT_InterfaceMethodref:
       return &ConstantInterfaceMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
    case CONSTANT_NameAndType:
       return &ConstantNameAndTypeInfo{}
    case CONSTANT_MethodType:
       return &ConstantMethodTypeInfo{}
    case CONSTANT_MethodHandle:
       return &ConstantMethodHandleInfo{}
    case CONSTANT_InvokeDynamic:
       return &ConstantInvokeDynamicInfo{}
    default:
       panic("java.lang.ClassFormatError: constant pool tag!")
    }
}
CONSTANT_Integer_info

CONSTANT_Integer_info使用4字节存储整数常量,其JVM结构定义如下:

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}

CONSTANT_Integer_info和后面将要介绍的其他三种数字常量无论是结构,还是实现,都非常相似,所以把它们定义在同一个文件中。创建cp_numeric.go文件,在其中定义 ConstantIntegerInfo结构体,代码如下:

package classfile
import "math"
type ConstantIntegerInfo struct {
	val int32
}
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {...}

readInfo()先读取一个uint32数据,然后把它转型成int32类型, 代码如下

func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {
    bytes := reader.readUint32()
    self.val = int32(bytes)
}
CONSTANT_Float_info

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

CONSTANT_Float_info {
    u1 tag;
    u4 bytes;
}

cp_numeric.go文件中定义ConstantFloatInfo结构体,代码如下:

type ConstantFloatInfo struct {
	val float32
}

func (self *ConstantFloatInfo) readInfo(reader *ClassReader) {
	bytes := reader.readUint32()
	self.val = math.Float32frombits(bytes)
}
CONSTANT_Long_info

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

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

cp_numeric.go文件中定义ConstantLongInfo结构体,代码如下:

type ConstantLongInfo struct {
    val int64
}

func (self *ConstantLongInfo) readInfo(reader *ClassReader) {
    bytes := reader.readUint64()
    self.val = int64(bytes)
}
CONSTANT_Double_info

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

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

cp_numeric.go文件中定义ConstantDoubleInfo结构体,代码如下:

type ConstantDoubleInfo struct {
    val float64
}

func (self *ConstantDoubleInfo) readInfo(reader *ClassReader) {
    bytes := reader.readUint64()
    self.val = math.Float64frombits(bytes)
}
CONSTANT_Utf8_info

CONSTANT_Utf8_info常量里放的是MUTF-8编码的字符串, 结构如下:

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

Java类文件中使用MUTF-8(Modified UTF-8)编码而不是标准的UTF-8,是因为MUTF-8在某些方面更适合于在Java虚拟机内部处理字符串。以下是一些原因:

  1. 空字符的表示: 在标准的UTF-8编码中,空字符(U+0000)会使用单个字节0x00表示,这与C字符串中的字符串终止符相同,可能引起混淆。在MUTF-8中,空字符会使用0xC0 0x80来表示,避免了混淆。
  2. 编码长度: MUTF-8编码中的每个字符都使用1至3个字节来表示,这与UTF-8编码相比更紧凑。对于大多数常见的字符集,这可以减少存储和传输开销。
  3. 字符的编码范围: MUTF-8编码对字符的范围进行了限制,只包含Unicode BMP(基本多文种平面)范围内的字符。这些字符通常足够用于表示Java标识符和字符串文字。
  4. 兼容性: 早期版本的Java使用的是MUTF-8编码,因此为了保持与早期版本的兼容性,后续版本也继续使用MUTF-8。这有助于确保Java类文件的可互操作性。

创建cp_utf8.go文件,在其中定义 ConstantUtf8Info结构体,代码如下:

type ConstantUtf8Info struct {
    str string
}

func (self *ConstantUtf8Info) readInfo(reader *ClassReader) {
    length := uint32(reader.readUint16())
    bytes := reader.readBytes(length)
    self.str = decodeMUTF8(bytes)
}

Java序列化机制也使用了MUTF-8编码。

java.io.DataInput和 java.io.DataOutput接口分别定义了readUTF()writeUTF()方法,可以读写MUTF-8编码的字符串。

如下为简化版的java.io.DataInputStream.readUTF()

// mutf8 -> utf16 -> utf32 -> string
func decodeMUTF8(bytearr []byte) string {
    utflen := len(bytearr)
    chararr := make([]uint16, utflen)

    var c, char2, char3 uint16
    count := 0
    chararr_count := 0

    for count < utflen {
       c = uint16(bytearr[count])
       if c > 127 {
          break
       }
       count++
       chararr[chararr_count] = c
       chararr_count++
    }

    for count < utflen {
       c = uint16(bytearr[count])
       switch c >> 4 {
       case 0, 1, 2, 3, 4, 5, 6, 7:
          /* 0xxxxxxx*/
          count++
          chararr[chararr_count] = c
          chararr_count++
       case 12, 13:
          /* 110x xxxx   10xx xxxx*/
          count += 2
          if count > utflen {
             panic("malformed input: partial character at end")
          }
          char2 = uint16(bytearr[count-1])
          if char2&0xC0 != 0x80 {
             panic(fmt.Errorf("malformed input around byte %v", count))
          }
          chararr[chararr_count] = c&0x1F<<6 | char2&0x3F
          chararr_count++
       case 14:
          /* 1110 xxxx  10xx xxxx  10xx xxxx*/
          count += 3
          if count > utflen {
             panic("malformed input: partial character at end")
          }
          char2 = uint16(bytearr[count-2])
          char3 = uint16(bytearr[count-1])
          if char2&0xC0 != 0x80 || char3&0xC0 != 0x80 {
             panic(fmt.Errorf("malformed input around byte %v", (count - 1)))
          }
          chararr[chararr_count] = c&0x0F<<12 | char2&0x3F<<6 | char3&0x3F<<0
          chararr_count++
       default:
          /* 10xx xxxx,  1111 xxxx */
          panic(fmt.Errorf("malformed input around byte %v", count))
       }
    }
    // The number of chars produced may be less than utflen
    chararr = chararr[0:chararr_count]
    runes := utf16.Decode(chararr)
    return string(runes)
}
  1. 初始化 chararr 数组,用于存储UTF-16字符。
  2. 遍历MUTF-8字节数组中的字节,根据字节的值来判断字符的编码方式。
  3. 如果字节值小于128,表示ASCII字符,直接转换为UTF-16并存储。
  4. 如果字节值在特定范围内,表示多字节字符,需要根据UTF-8编码规则进行解码。
  5. 如果遇到不符合规则的字节,抛出异常来处理错误情况。
  6. 最后,将解码后的UTF-16字符转换为Go字符串。
CONSTANT_String_info

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

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

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

下创建cp_string.go文件,在其中定义 ConstantStringInfo结构体

type ConstantStringInfo struct {
    cp ConstantPool
    stringIndex uint16
}

func (self *ConstantStringInfo) readInfo(reader *ClassReader) {
    self.stringIndex = reader.readUint16()
}

String()方法按索引从常量池中查找字符串:

func (self *ConstantStringInfo) String() string {
    return self.cp.getUtf8(self.stringIndex)
}
CONSTANT_Class_info

CONSTANT_Class_info常量表示类或者接口的符号引用

他是对类或者接口的符号引用。它描述的可以是当前类型的信息,也可以描述对当前类的引用,还可以描述对其他类的引用。JVM结构如下:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

CONSTANT_String_info类似,name_index是常量池索引,指向CONSTANT_Utf8_info常量。 创建 cp_class.go文件,定义ConstantClassInfo结构体

type ConstantClassInfo struct {
    cp        ConstantPool
    nameIndex uint16
}

func (self *ConstantClassInfo) readInfo(reader *ClassReader) {
    self.nameIndex = reader.readUint16()
}
func (self *ConstantClassInfo) Name() string {
    return self.cp.getUtf8(self.nameIndex)
}
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_indexdescriptor_index都是常量池索引,指向CONSTANT_Utf8_info常量

Java虚拟机规范定义了一种简单的语法来描述字段和方法,可以根据下面的规则生成描述符。

一、类型描述符

  1. 基本类型byte、short、char、int、long、float和double的描述符是单个字母,分别对应B、S、C、I、J、F和D。注意,long的描述符是J 而不是L。
  2. 引用类型的描述符是L+类的完全限定名+分号。
  3. 数组类型的描述符是[+数组元素类型描述符

二、字段描述符

​ 字段类型的描述符

三、方法描述符

​ 分号分隔的参数类型描述符+返回值类型描述符,其中void返回值由单个字母V表示。

Java语言支持方法重载(override),不同的方法可 以有相同的名字,只要参数列表不同即可。 这就是为什么 CONSTANT_NameAndType_info结构要同时包含名称和描述符的原因。

创建cp_name_and_type.go文件,在其中定义ConstantName-AndTypeInfo结构体,代码如下:

type ConstantNameAndTypeInfo struct {
    nameIndex       uint16
    descriptorIndex uint16
}

func (self *ConstantNameAndTypeInfo) readInfo(reader *ClassReader) {
    self.nameIndex = reader.readUint16()
    self.descriptorIndex = reader.readUint16()
}
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_indexname_and_type_index都是常量池索引,分别指向 CONSTANT_Class_infoCONSTANT_NameAndType_info常量。

创建cp_member_ref.go文件,定义一个统一的结构体ConstantMemberrefInfo来表示这3种常量,然后定义三个结构体“继承”ConstantMemberrefInfo

Go语言并没有“继承”这个概念,但是可以通过结构体嵌套来模拟

type ConstantFieldrefInfo struct{ ConstantMemberrefInfo }
type ConstantMethodrefInfo struct{ ConstantMemberrefInfo }
type ConstantInterfaceMethodrefInfo struct{ ConstantMemberrefInfo }

type ConstantMemberrefInfo struct {
    cp               ConstantPool
    classIndex       uint16
    nameAndTypeIndex uint16
}

func (self *ConstantMemberrefInfo) readInfo(reader *ClassReader) {
    self.classIndex = reader.readUint16()
    self.nameAndTypeIndex = reader.readUint16()
}
func (self *ConstantMemberrefInfo) ClassName() string {
	return self.cp.getClassName(self.classIndex)
}
func (self *ConstantMemberrefInfo) NameAndDescriptor() (string, string) {
	return self.cp.getNameAndType(self.nameAndTypeIndex)
}

还有三个常量没有介绍:CONSTANT_MethodType_info、 CONSTANT_MethodHandle_info和 CONSTANT_InvokeDynamic_info。它们是Java SE 7才添加到class文件中的,目的是支持新增的invokedynamic指令。本次暂不实现。

总结

可以把常量池中的常量分为两类:字面量(literal)符号引用 (symbolic reference)

字面量包括数字常量字符串常量符号引用包括接口名字段方法信息等。

除了字面量,其他常量都是通过索引直接或间接指向CONSTANT_Utf8_info常量,以 CONSTANT_Fieldref_info为例,如下所示。

1.2.4解析属性表

一些重要的信息没有出现,如方法的字节码等。那么这些信息存在哪里呢?答案是属性表。

AttributeInfo接口

和常量池类似,各种属性表达的信息也各不相同,因此无法用统一的结构来定义。不同之处在于,常量是由Java虚拟机规范严格 定义的,共有14种。

但属性是可以扩展的,不同的虚拟机实现可以定义自己的属性类型。

由于这个原因,Java虚拟机规范没有使用tag,而是使用属性名来区别不同的属性。

属性数据放在属性名之后的u1表中,这样Java虚拟机实现就可以跳过自己无法识别的属性。 属性的结构定义如下:

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

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

创建attribute_info.go文件,在其中定义 AttributeInfo接口

package classfile
type AttributeInfo interface {
	readInfo(reader *ClassReader)
}
func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {...}
func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {...}
func newAttributeInfo(attrName string, attrLen uint32,cp ConstantPool) AttributeInfo {...}

ConstantInfo接口一样,AttributeInfo接口也只定义了一个readInfo()方法,需要由具体的属性实现。readAttributes()函数读取属性表。

func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {
    attributesCount := reader.readUint16()
    attributes := make([]AttributeInfo, attributesCount)
    for i := range attributes {
       attributes[i] = readAttribute(reader, cp)
    }
    return attributes
}

读取单个属性函数:

func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {
    attrNameIndex := reader.readUint16()
    attrName := cp.getUtf8(attrNameIndex)
    attrLen := reader.readUint32()
    attrInfo := newAttributeInfo(attrName, attrLen, cp)
    attrInfo.readInfo(reader)
    return attrInfo
}

readAttribute()先读取属性名索引,根据它从常量池中找到属性名,然后读取属性长度,接着调用newAttributeInfo()函数创建具体的属性实例。

Java虚拟机规范预定义了23种属性,先解析其中的8种。newAttributeInfo()函数的代码如下

func newAttributeInfo(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo {
    switch attrName {
    case "Code":
       return &CodeAttribute{cp: cp}
    case "ConstantValue":
       return &ConstantValueAttribute{}
    case "Deprecated":
       return &DeprecatedAttribute{}
    case "Exceptions":
       return &ExceptionsAttribute{}
    case "LineNumberTable":
       return &LineNumberTableAttribute{}
    case "LocalVariableTable":
       return &LocalVariableTableAttribute{}
    case "SourceFile":
       return &SourceFileAttribute{cp: cp}
    case "Synthetic":
       return &SyntheticAttribute{}
    default:
       return &UnparsedAttribute{attrName, attrLen, nil}
    }
}

创建attr_unparsed.go文件中,定义UnparsedAttribute结构体

package classfile

/*
attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}
*/
type UnparsedAttribute struct {
    name   string
    length uint32
    info   []byte
}

func (self *UnparsedAttribute) readInfo(reader *ClassReader) {
    self.info = reader.readBytes(self.length)
}

func (self *UnparsedAttribute) Info() []byte {
    return self.info
}

按照用途,23种预定义属性可以分为三组。

  • 第一组属性是实现 Java虚拟机所必需的,共有5种;
  • 第二组属性是Java类库所必需的,共有12种;
  • 第三组属性主要提供给工具使用,共有6种。

第三组属性是可选的,也就是说可以不出现在class文件中。如果class文件中存在第三组属性,Java虚拟机实现或者Java类库也是可以利用它们 的,比如使用LineNumberTable属性在异常堆栈中显示行号。

如下给出了这23 种属性出现的Java版本、分组以及它们在class文件中的位置。

Deprecated和Synthetic属性

DeprecatedSynthetic是最简单的两种属性,仅起标记作用,不包含任何数据。

这两种属性都是JDK1.1引入的,可以出现在 ClassFile、field_info和method_info结构中,它们的结构定义如下:

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

由于不包含任何数据,所以attribute_length的值必须是0。

Deprecated属性用于指出类、接口、字段或方法已经不建议使用,编译器等工具可以根据Deprecated属性输出警告信息。

J2SE 5.0之前 可以使用Javadoc提供的@deprecated标签指示编译器给类、接口、字段或方法添加Deprecated属性,语法格式如下:

/** @deprecated */
public void oldMethod() {...}

J2SE 5.0开始,也可以使用@Deprecated注解,语法格式如下:

@Deprecated
public void oldMethod() {}

在Java中,编译器可能会生成一些额外的方法、字段或类,用于支持内部的匿名内部类、枚举、泛型等特性。这些生成的元素可能会被标记为 Synthetic

创建attr_markers.go文件,在其中定义 DeprecatedAttributeSyntheticAttribute结构体,代码如下:

package classfile
type DeprecatedAttribute struct { MarkerAttribute }
type SyntheticAttribute struct { MarkerAttribute }
type MarkerAttribute struct{}
func (self *MarkerAttribute) readInfo(reader *ClassReader) {
// read nothing
}
SourceFile属性

SourceFile 属性是Java类文件中的一个属性,它用于指定源文件的名称,即生成该类文件的源代码文件的名称。这个属性并不直接影响类的运行时行为。其结构定义如下:

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

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

创建 attr_source_file.go文件,在其中定义SourceFileAttribute结构体,代码如下:

package classfile
type SourceFileAttribute struct {
    cp ConstantPool
    sourceFileIndex uint16
}
func (self *SourceFileAttribute) readInfo(reader *ClassReader) {
	self.sourceFileIndex = reader.readUint16()
}
func (self *SourceFileAttribute) FileName() string {
	return self.cp.getUtf8(self.sourceFileIndex)
}

例如,如果有一个名为 MyClass.java 的源代码文件,它包含以下类:

public class MyClass {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

当编译 MyClass.java 文件时,会生成一个名为 MyClass.class 的类文件,并在其中添加一个 SourceFile 属性,将其值设置为 MyClass.java

ConstantValue属性

ConstantValue 属性是Java类文件中的一个属性,通常与字段(field)相关联。这个属性的作用是为字段提供一个常量初始值。这意味着,如果您在类中声明一个字段,并为其分配了 ConstantValue 属性,那么该字段的初始值将在类加载时被设置为 ConstantValue 中指定的常量。

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

例如,假设有以下Java代码:

public class MyClass {
    public final int myField = 42;
}

在对应的类文件中,将包含一个 ConstantValue 属性,指定了常量值 42,并与 myField 字段相关联。当类加载时,myField 将被初始化为 42

constantvalue_index是常量池索引,具体指向哪种常量因字段类型而异,如下为对照表

创建attr_constant_value.go文件,在其中定义ConstantValueAttribute结构体,代码如下:

package classfile
type ConstantValueAttribute struct {
	constantValueIndex uint16
}
func (self *ConstantValueAttribute) readInfo(reader *ClassReader) {
	self.constantValueIndex = reader.readUint16()
}
func (self *ConstantValueAttribute) ConstantValueIndex() uint16 {
	return self.constantValueIndex
}

Code属性

Code 属性是Java类文件中的一个属性,通常与方法(Method)相关联。

它包含了方法的字节码指令,即实际的可执行代码。Code 属性是Java类文件中最重要的属性之一,因为它包含了方法的实际执行逻辑。

以下是关于 Code 属性的一些重要信息:

  1. 属性结构Code 属性通常包含以下信息:
    • 最大堆栈深度(max_stack):方法执行时所需的最大堆栈深度。
    • 局部变量表的大小(max_locals):方法内部局部变量表的大小。
    • 字节码指令(code):实际的字节码指令序列,即方法的执行代码。
    • 异常处理器列表(exception_table):用于捕获和处理异常的信息。
    • 方法属性(attributes):其他与方法相关的属性,例如局部变量表、行号映射表等。
  2. 字节码指令Code 属性中的 code 部分包含了方法的实际字节码指令,这些指令由Java虚拟机执行。每个指令执行一些特定的操作,例如加载、存储、算术操作、分支、方法调用等。
  3. 异常处理Code 属性中的 exception_table 部分包含了异常处理器的信息,指定了哪些字节码范围可以抛出哪些异常,并且指定了如何处理这些异常。
  4. 局部变量表Code 属性中的局部变量表(max_locals)用于存储方法执行期间的局部变量,例如方法参数和临时变量。
  5. 属性Code 属性中还可以包含其他属性,如局部变量表、行号映射表等,这些属性提供了更多的调试和运行时信息。

Code 属性是Java虚拟机实际执行方法的关键部分,它描述了方法的行为和操作,包括如何处理输入和生成输出。编译器将源代码编译为字节码,然后将字节码填充到 Code 属性中,这使得Java程序可以在虚拟机上执行。

创建attr_code.go文件,定义CodeAttribute结构体ExceptionTableEntry结构体,代码如下:

type CodeAttribute struct {
	cp             ConstantPool
	maxStack       uint16
	maxLocals      uint16
	code           []byte
	exceptionTable []*ExceptionTableEntry
	attributes     []AttributeInfo
}

func (self *CodeAttribute) readInfo(reader *ClassReader) {
	self.maxStack = reader.readUint16()
	self.maxLocals = reader.readUint16()
	codeLength := reader.readUint32()
	self.code = reader.readBytes(codeLength)
	self.exceptionTable = readExceptionTable(reader)
	self.attributes = readAttributes(reader, self.cp)
}
type ExceptionTableEntry struct {
    startPc   uint16
    endPc     uint16
    handlerPc uint16
    catchType uint16
}

func readExceptionTable(reader *ClassReader) []*ExceptionTableEntry {
    exceptionTableLength := reader.readUint16()
    exceptionTable := make([]*ExceptionTableEntry, exceptionTableLength)
    for i := range exceptionTable {
       exceptionTable[i] = &ExceptionTableEntry{
          startPc:   reader.readUint16(),
          endPc:     reader.readUint16(),
          handlerPc: reader.readUint16(),
          catchType: reader.readUint16(),
       }
    }
    return exceptionTable
}
Exceptions属性

Exceptions属性通常与方法(Method)相关联,用于指定方法可能抛出的受检查异常(checked exceptions)的列表。

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

创建attr_exceptions.go文件,在其中定义ExceptionsAttribute结构体

type ExceptionsAttribute struct {
    exceptionIndexTable []uint16
}

func (self *ExceptionsAttribute) readInfo(reader *ClassReader) {
    self.exceptionIndexTable = reader.readUint16s()
}

func (self *ExceptionsAttribute) ExceptionIndexTable() []uint16 {
    return self.exceptionIndexTable
}
LineNumberTable和LocalVariableTable属性

LineNumberTableLocalVariableTable 属性是Java类文件中的两个用于调试和运行时跟踪的属性,它们包含了与源代码中行号和局部变量相关的信息。

LineNumberTable 属性:用于建立源代码行号和字节码指令之间的映射。它允许开发工具在调试时将异常栈轨迹映射到源代码的特定行,以便开发者可以更容易地定位和修复代码中的问题。结构如下:

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

LocalVariableTable 属性:用于跟踪局部变量的信息,包括局部变量的名称、数据类型、作用域范围和字节码偏移。

创建attr_line_number_table.go文件,定义LineNumberTableAttribute结构体,代码如下:

type LineNumberTableAttribute struct {
    lineNumberTable []*LineNumberTableEntry
}

type LineNumberTableEntry struct {
    startPc    uint16
    lineNumber uint16
}

func (self *LineNumberTableAttribute) readInfo(reader *ClassReader) {
    lineNumberTableLength := reader.readUint16()
    self.lineNumberTable = make([]*LineNumberTableEntry, lineNumberTableLength)
    for i := range self.lineNumberTable {
       self.lineNumberTable[i] = &LineNumberTableEntry{
          startPc:    reader.readUint16(),
          lineNumber: reader.readUint16(),
       }
    }
}

func (self *LineNumberTableAttribute) GetLineNumber(pc int) int {
    for i := len(self.lineNumberTable) - 1; i >= 0; i-- {
       entry := self.lineNumberTable[i]
       if pc >= int(entry.startPc) {
          return int(entry.lineNumber)
       }
    }
    return -1
}

创建attr_local_variable_table.go文件,定义LocalVariableTableAttribute,代码如下:

type LocalVariableTableAttribute struct {
    localVariableTable []*LocalVariableTableEntry
}

type LocalVariableTableEntry struct {
    startPc         uint16
    length          uint16
    nameIndex       uint16
    descriptorIndex uint16
    index           uint16
}

func (self *LocalVariableTableAttribute) readInfo(reader *ClassReader) {
    localVariableTableLength := reader.readUint16()
    self.localVariableTable = make([]*LocalVariableTableEntry, localVariableTableLength)
    for i := range self.localVariableTable {
       self.localVariableTable[i] = &LocalVariableTableEntry{
          startPc:         reader.readUint16(),
          length:          reader.readUint16(),
          nameIndex:       reader.readUint16(),
          descriptorIndex: reader.readUint16(),
          index:           reader.readUint16(),
       }
    }
}

1.3测试

打开ch03\main.go文件,修改import语句和startJVM()函数,代码如下:

package main

import "fmt"
import "strings"
import "jvmgo/ch03/classfile"
import "jvmgo/ch03/classpath"

func main() {
    cmd := parseCmd()

    if cmd.versionFlag {
       fmt.Println("version 0.0.1")
    } else if cmd.helpFlag || cmd.class == "" {
       printUsage()
    } else {
       startJVM(cmd)
    }
}

func startJVM(cmd *Cmd) {
    cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
    className := strings.Replace(cmd.class, ".", "/", -1)
    cf := loadClass(className, cp)
    fmt.Println(cmd.class)
    printClassInfo(cf)
}

func loadClass(className string, cp *classpath.Classpath) *classfile.ClassFile {
    classData, _, err := cp.ReadClass(className)
    if err != nil {
       panic(err)
    }

    cf, err := classfile.Parse(classData)
    if err != nil {
       panic(err)
    }

    return cf
}

func printClassInfo(cf *classfile.ClassFile) {
    fmt.Printf("version: %v.%v\n", cf.MajorVersion(), cf.MinorVersion())
    fmt.Printf("constants count: %v\n", len(cf.ConstantPool()))
    fmt.Printf("access flags: 0x%x\n", cf.AccessFlags())
    fmt.Printf("this class: %v\n", cf.ClassName())
    fmt.Printf("super class: %v\n", cf.SuperClassName())
    fmt.Printf("interfaces: %v\n", cf.InterfaceNames())
    fmt.Printf("fields count: %v\n", len(cf.Fields()))
    for _, f := range cf.Fields() {
       fmt.Printf("  %s\n", f.Name())
    }
    fmt.Printf("methods count: %v\n", len(cf.Methods()))
    for _, m := range cf.Methods() {
       fmt.Printf("  %s\n", m.Name())
    }
}

首先go install jvmgo\ch03 生产ch03.exe

然后执行,并输入命令行语句,得到结果如下:

  • version: 52.0:这表示 java.lang.String 类的类文件版本为 52.0。类文件版本号与Java版本号有关,52.0 对应于Java 8。
  • constants count: 548:这表示常量池中包含 548 个常量。常量池包含了类的常量、方法、字段等信息。
  • access flags: 0x31:这表示类的访问标志,0x31 是十六进制表示,对应于二进制 00110001。这些标志描述类的访问权限和特性。
  • this class: java/lang/String:这表示类的名称,即 java.lang.String
  • super class: java/lang/Object:这表示 java.lang.String 类继承自 java.lang.Object 类。
  • interfaces: [java/io/Serializable java/lang/Comparable java/lang/CharSequence]:这表示 java.lang.String 类实现了三个接口,分别是 SerializableComparableCharSequence
  • fields count: 5:这表示 java.lang.String 类包含 5 个字段。
  • methods count: 94:这表示 java.lang.String 类包含 94 个方法。其中一些是构造方法(<init>),其他是实例方法。