ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

【DIY系列:Java虚拟机】第14篇:类索引、接口表与字段方法表——class 文件的“族谱“

【DIY系列:Java虚拟机】第14篇:类索引、接口表与字段方法表——class 文件的“族谱“ 上一篇【第13篇】类访问标志——public/abstract/final 都在里面下一篇【第15篇】常量池概述——class 文件的宝藏库摘要上一篇搞定了访问标志这一篇解析类的族谱我是谁this_class、我爹是谁super_class、我实现了哪些接口interfaces、以及我有哪些字段和方法。这部分会引出 JVM 里一个极其重要的概念——描述符descriptor。你可能见过([Ljava/lang/String;)V这种天书般的字符串它就是 main 方法的描述符。本文彻底讲透这套类型表示法为什么 JVM 不用int、String这种直观写法而要用I、Ljava/lang/String;这种怪符号。读完这篇你就能完整读出一个类叫什么、继承谁、实现哪些接口、有哪些字段和方法。一、this_class 与 super_class我是谁我爹是谁访问标志之后紧跟着两个 u2u2 this_class;// 类索引u2 super_class;// 超类索引这两个值不是字符串而是指向常量池的索引。【类索引与超类索引的间接引用】 class 文件 常量池 ┌──────────────────┐ ┌─────────────────────────────┐ │ this_class 5 │──────────► │ #5: CONSTANT_Class_info │ └──────────────────┘ 指向 │ name_index 32 │ └──────────────┬──────────────┘ ┌──────────────────┐ │ 指向 │ super_class 7 │──────────► ┌──────────────▼──────────────┐ └──────────────────┘ │ #32: CONSTANT_Utf8_info │ │ java/lang/String │◄── 真正的类名 └─────────────────────────────┘ 所以要拿到类名要跳两次 this_class (索引) → CONSTANT_Class_info → CONSTANT_Utf8_info → java/lang/String为什么要绕这么大弯为了复用。一个 class 文件里java/lang/Object这个类名可能出现在几十个地方父类、各种方法签名…。如果每处都存一遍完整字符串文件会臃肿好几倍。用常量池索引每处只存 2 字节字符串只存一份。从常量池查类名// ch03/classfile/constant_pool.go// getClassName 从常量池查找类名func(self ConstantPool)getClassName(indexuint16)string{// 1. 先取出 CONSTANT_Class_infoclassInfo:self.getConstantInfo(index).(*ConstantClassInfo)// 2. 再从它的 name_index 取出 Utf8 字符串returnself.getUtf8(classInfo.nameIndex)}// getUtf8 从常量池查找 Utf8 字符串func(self ConstantPool)getUtf8(indexuint16)string{utf8Info:self.getConstantInfo(index).(*ConstantUtf8Info)returnutf8Info.str}常量池的具体实现在下一篇这里先知道接口即可。super_class 的特例0唯一一个 super_class 为 0 的类是java.lang.Object——它是所有类的祖先没有父类。func(self*ClassFile)SuperClassName()string{ifself.superClass0{returnself.constantPool.getClassName(self.superClass)}return// java.lang.Object 没有父类}【super_class 的特例】 java.lang.Object this_class 15 → java/lang/Object super_class 0 ← 特例没有父类 其它所有类 this_class N → com/example/Hello super_class M → java/lang/Object或别的父类 接口interface呢 接口的 super_class 也是有效的 它指向 java/lang/Object接口隐含继承 Object重点判断一个类是不是java.lang.Object最简单的办法就是看super_class 0。后面实现类继承体系时会用到这个判据。二、interfaces接口索引表超类索引之后是接口表u2 interfaces_count;// 接口数量u2 interfaces[interfaces_count];// 接口索引数组这是一个典型的表结构u2 计数 N 个 u2 表项。每个表项都是指向常量池CONSTANT_Class_info的索引。// 读取接口表一行搞定self.interfacesreader.readUint16s()// 获取接口名列表func(self*ClassFile)InterfaceNames()[]string{interfaceNames:make([]string,len(self.interfaces))fori,cpIndex:rangeself.interfaces{interfaceNames[i]self.constantPool.getClassName(cpIndex)}returninterfaceNames}【接口索引表示例java.lang.String】 interfaces_count 3 interfaces[0] 45 → CONSTANT_Class #45 → java/io/Serializable interfaces[1] 78 → CONSTANT_Class #78 → java/lang/Comparable interfaces[2] 92 → CONSTANT_Class #92 → java/lang/CharSequence 对应 Java 源码 public final class String implements java.io.Serializable, java.lang.ComparableString, java.lang.CharSequence { }注意一点接口表只包含直接实现的接口不包含间接继承的。比如interfaceA{}interfaceBextendsA{}classCimplementsB{}// C 的 interfaces 表只有 [B]不包含 A要拿到A需要递归遍历B的 interfaces 表。这个递归查找在第 6 章实现接口方法解析时会用到。三、MemberInfo字段和方法表的统一表示字段表和方法表的结构完全一样所以 JVM 规范和我们的实现用同一个结构体表示field_info/method_info{u2 access_flags;// 访问标志u2 name_index;// 名字索引 → 常量池 Utf8u2 descriptor_index;// 描述符索引 → 常量池 Utf8u2 attributes_count;// 属性数量attribute_info attributes[attributes_count];// 属性表}四个字段 一个属性表字段作用access_flags访问标志public/private/static/final…见上一篇name_index字段名/方法名指向常量池的 Utf8descriptor_index描述符类型信息指向常量池的 Utf8attributes属性表方法的话最重要的就是 Code 属性Go 实现// ch03/classfile/member_info.gopackageclassfile// MemberInfo 表示字段或方法两者的结构完全一样typeMemberInfostruct{cp ConstantPool// 常量池用于查名字和描述符accessFlagsuint16// 访问标志nameIndexuint16// 名字索引descriptorIndexuint16// 描述符索引attributes[]AttributeInfo// 属性表}// readMembers 读取字段表或方法表funcreadMembers(reader*ClassReader,cp ConstantPool)[]*MemberInfo{memberCount:reader.readUint16()// 先读数量members:make([]*MemberInfo,memberCount)fori:rangemembers{members[i]readMember(reader,cp)}returnmembers}// readMember 读取单个字段或方法funcreadMember(reader*ClassReader,cp ConstantPool)*MemberInfo{returnMemberInfo{cp:cp,accessFlags:reader.readUint16(),nameIndex:reader.readUint16(),descriptorIndex:reader.readUint16(),attributes:readAttributes(reader,cp),}}// Getter 方法func(self*MemberInfo)AccessFlags()uint16{returnself.accessFlags}func(self*MemberInfo)Name()string{returnself.cp.getUtf8(self.nameIndex)}func(self*MemberInfo)Descriptor()string{returnself.cp.getUtf8(self.descriptorIndex)}注意readMembers被调用了两次——一次读字段表一次读方法表func(self*ClassFile)read(reader*ClassReader){// ...self.fieldsreadMembers(reader,self.constantPool)// 字段表self.methodsreadMembers(reader,self.constantPool)// 方法表// ...}重点字段和方法的结构完全相同区别只在于它们出现在文件的什么位置、以及属性表的内容不同字段可能有 ConstantValue 属性方法一定有 Code 属性。这种统一设计简化了实现。四、描述符DescriptorJVM 的类型表示法现在到了本文最重要的部分——描述符。什么是描述符描述符是 JVM 用来表示类型的一套字符串编码。你可能已经见过它比如在javap -v的输出里descriptor: ([Ljava/lang/String;)V这是main方法的描述符意思是“参数是 String[]返回值是 void”。为什么需要描述符因为 Java 是强类型语言JVM 需要在很多场合精确描述类型而且必须无歧义、紧凑。Java 源码的写法int、String[]不够用因为有歧义String[]和String []是同一个类型但字符串不同不够紧凑java.lang.String[]要 20 个字符无法表达某些 JVM 内部类型描述符用一套紧凑的符号解决了这些问题。基本类型描述符每个基本类型用一个大写字母表示描述符Java 类型助记BbyteByteCcharCharDdoubleDoubleFfloatFloatIintIntJlong为啥是 JI 被 int 占了J 是 I 的下一个字母SshortShortZboolean为啥是 ZB 被 byte 占了Z 谐音真值VvoidVoid只用于方法返回值记忆技巧大部分是首字母只有两个例外——long用JI 被 int 占了boolean用ZB 被 byte 占了。引用类型描述符对象类型用L 全限定名 ;表示【引用类型描述符格式】 L 全限定名用 / 分隔 ; │ │ └─ 开头标记 └─ 结尾分号不能少 例子 java.lang.String → Ljava/lang/String; java.lang.Object → Ljava/lang/Object; java.util.List → Ljava/util/List; com.example.Hello → Lcom/example/Hello; 注意两点 1. 包名分隔符用【斜线 /】而不是点号 . 2. 结尾的【分号 ; 】不能省略为什么要结尾分号为了消除歧义。考虑方法描述符(Ljava/lang/String;)V——如果没有结尾分号Ljava/lang/StringV就无法判断类名在哪里结束。分号是明确的类名到此为止标记。数组类型描述符数组用左方括号[作为前缀维度就是[的个数Java 类型描述符int[][Iint[][][[IString[][Ljava/lang/String;String[][][[Ljava/lang/String;Object[][Ljava/lang/Object;【数组描述符的规则】 规则[ 的个数 数组维度后面跟元素类型的描述符 int[] → [I │└─ int └─ 1 维 int[][] → [[I │└─ int └┴─ 2 维 String[] → [Ljava/lang/String; │└──────┬────────┘ │ └─ String 类型L...; 形式 └─ 1 维 String[][] → [[Ljava/lang/String;方法描述符方法描述符的格式(参数类型列表)返回值类型【方法描述符格式】 ( 参数1描述符 参数2描述符 ... ) 返回类型描述符 参数列表所有参数描述符【直接连在一起】不用分隔符 返回值单个描述符void 用 V 例子 void main(String[] args) → ([Ljava/lang/String;)V │└──────┬──────┘││ │ │ │└─ V void 返回值 │ │ └─ 参数列表结束 │ └───────── [Ljava/lang/String; String[] └─ 参数列表开始 int add(int a, int b) → (II)I ││ │ ││ └─ 返回 int └┴── 两个 int 参数直接相连无分隔符 String getName() → ()Ljava/lang/String; │└──────┬────────┘ │ └─ 返回 String └─ 无参数 void setName(String name) → (Ljava/lang/String;)V boolean isEmpty(int[] arr, String s) → ([ILjava/lang/String;)Z描述符对照表速查Java 方法声明描述符void main(String[] args)([Ljava/lang/String;)Vint add(int a, int b)(II)IString getName()()Ljava/lang/String;void setName(String s)(Ljava/lang/String;)Vlong currentTimeMillis()()Jdouble sqrt(double d)(D)Dboolean equals(Object o)(Ljava/lang/Object;)Zvoid print(int[] arr, String s)([ILjava/lang/String;)VString[][] getMatrix()()[[Ljava/lang/String;Java 字段声明描述符int countIString nameLjava/lang/String;int[] numbers[Iboolean flagZdouble[][] matrix[[D重点描述符是理解字节码的钥匙。后面实现方法调用、字段访问、类型检查时处处都要解析描述符。建议现在就把它记牢。五、实战读出一个类的完整信息把前面所有内容整合我们改写startJVM来打印完整的类信息// ch03/main.gofuncprintClassInfo(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:rangecf.Fields(){fmt.Printf( %s\t%s\n,f.Name(),f.Descriptor())}fmt.Printf(methods count: %v\n,len(cf.Methods()))for_,m:rangecf.Methods(){fmt.Printf( %s\t%s\n,m.Name(),m.Descriptor())}}测试一个自己写的类// D:\test\HelloWorld.javapublicclassHelloWorld{privatestaticfinalintMAX100;privateStringname;publicstaticvoidmain(String[]args){System.out.println(Hello, world!);}publicStringgetName(Stringprefix){returnprefixname;}}编译并解析javac-dD:\test D:\test\HelloWorld.java ch03.exe-XjreC:\Java\jdk1.8.0_202\jre-cpD:\test HelloWorld输出version: 52.0 constants count: 34 access flags: 0x21 this class: HelloWorld super class: java/lang/Object interfaces: [] fields count: 2 MAX I name Ljava/lang/String; methods count: 3 init ()V main ([Ljava/lang/String;)V getName (Ljava/lang/String;)Ljava/lang/String;完整解读输出含义version: 52.0JDK 8 编译constants count: 34常量池 33 项count - 1access flags: 0x21PUBLIC SUPER普通 public 类this class: HelloWorld本类名super class: java/lang/Object默认继承 Objectinterfaces: []没实现接口MAX → Iprivate static final int MAXname → Ljava/lang/String;private String nameinit → ()V默认构造器编译器生成main → ([Ljava/lang/String;)Vmain 方法getName → (Ljava/lang/String;)Ljava/lang/String;返回 String 的方法注意几个细节init是编译器自动生成的构造器——源码里没写但 class 文件里有MAX字段的描述符是I——static final这些修饰符不体现在描述符里它们在 access_flags 里getName的描述符参数 String、返回值 String一目了然用javap -v对照验证javap-v-pD:\test\HelloWorld.class会看到完全一致的信息plus 常量池、Code 属性等。本篇小结本篇解析了类的族谱信息this_class / super_class指向常量池的索引要跳两次才能拿到真正的类名索引 → Class_info → Utf8_infosuper_class 0 的特例只有java.lang.Object没有父类interfaces 表u2 计数 N 个 u2 索引只含直接实现的接口不含间接继承的MemberInfo字段和方法结构完全相同用同一个结构体表示access_flags name_index descriptor_index attributes描述符DescriptorJVM 的类型表示法基本类型I(int)J(long)Z(boolean)D(double)V(void) 等引用类型L全限定名;用/分隔结尾必须有;数组[前缀维度 [的个数方法(参数描述符)返回值描述符参数直接相连无分隔符实战验证成功读出 HelloWorld 的字段和方法包括编译器自动生成的init构造器到这里class 文件的骨架魔数、版本、常量池引用、访问标志、类关系、字段方法表已经清楚了。但还有两大块没解决常量池next和属性表第 021-024 篇。下一篇进入整个系列最硬核的部分——常量池。它是 class 文件的宝藏库占文件 60% 以上的空间有 14 种不同类型的常量。我们会在接下来的 6 篇文章里把它彻底啃下来。上一篇【第13篇】类访问标志——public/abstract/final 都在里面下一篇【第15篇】常量池概述——class 文件的宝藏库
返回列表