class文件格式的理解

以前了解JVM相关知识时,对类文件结构仅仅是一瞥而过,并未仔细去深究,实在不应该。于是最近找了个闲暇,来仔细理解一下。

先不谈各种定义,就现象而言,class文件只是开发者编译java文件产生的产物,且这个产物是供JVM阅读的。
class文件于Java程序员自然随手可拾,当使用文本编辑器打开的时候却是这样的:

cafe babe 0000 0034 003d 0a00 1100 2909
0010 002a 0900 1000 2b09 0010 002c 0900
1000 2d07 002e 0a00 0600 2908 002f 0a00
...
...

第一感觉就是除了"cafe"这4个字母比较眼熟,其他这些hex字符串光凭肉眼又哪里能知道是什么意思。
所以,肯定是打开的方式不对?

严格来说,这样打开并无问题,因为class文件本身就是字节码,本来页面就是如此。之所以不知道这些字节码代表的什么意思,只是因为不了解class文件的格式而已。

而我们能在各种资料书籍中找到各种对class文件格式的定义,比如Constant_Class_info,比如Field_info…诸如此类的东西,就是帮助剖析这些字节码。

虽然,javap命令就可以帮助开发者快速剖析。我们只要使用

javap -verbose [class文件路径]

就能得到详细的结果。
当然得到结果并不代表能够理解结果的意义,所以最终还是得知晓定义才行。

整体结构

就标准而言,整个class文件格式按顺序是存储着下面这个表格里面的内容:

字段表长度含义
magic4 byte万年不变的"cafebabe"
minor_version2 byte当前JDK副版本号
major_version2 byte当前JDK主版本号
constant_pool_count2 byte常量池数量,代表着此类文件中常量的数量
constant_pool[constant_pool_count-1]n byte各种常量表,每种常量表的结构与长度有所差别
access_flags2 byte访问标志,即public/final等修饰符
this_class2 byte类索引
super_class2 byte父类索引
interfaces_count2 byte接口数量
interfaces[interfaces_count]2*n byte每个接口表仅2byte,即只存储索引值
fields_count2 byte字段数量
fields[fields_count]n byte字段表结构相同,但因属性的缘故,长度有所差别
methods_count2 byte方法数量
methods[methods_count]n byte与字段表结构类似
attributes_count2 byte属性数量
attributes[attributes_count]n byte各种属性表,每种属性表的结构与长度有所差别

这张表在资料里面应该是很常见的。我们可以稍微捋一捋:

  1. 固定字段8个byte,与类本身并没有太大关系,每个class文件都有
  2. 常量池,长度不定,存储了各种常量
  3. 类信息,固定字段3*2byte,类访问标志、类索引、父类索引
  4. 接口,长度不定,存储了各个接口对应的索引
  5. 字段,长度不定,存储了各种字段
  6. 方法,长度不定,存储了各种方法
  7. 属性,长度不定,存储了各种属性

需要声明的是,这其中出现的索引,最终都会指向常量池的utf8字符串的值。
比如我们在使用javap命令后会看到这样的信息:

   #2 = Fieldref           #16.#42        // com/xter/design/builder/Point.x:I
   #3 = Fieldref           #16.#43        // com/xter/design/builder/Point.y:I

毫无疑问,这是常量池中的字段,而#16/#42正是常量池其他值的索引中的两个:

  #16 = Class              #55            // com/xter/design/builder/Point
  #42 = NameAndType        #18:#19        // x:I
  #43 = NameAndType        #20:#19        // y:I

而这三个常量,又指向其他的索引:

  #18 = Utf8               x
  #19 = Utf8               I
  #20 = Utf8               y
  #55 = Utf8               com/xter/design/builder/Point

可以看到最终是指向utf8类型的常量值。而javap很体贴地将最终指向的utf8值都注释在后面了。

常量池

就JDK1.8而言,常量有14种,每种常量都有自己的结构,但他们的相同之处是第1个byte代表自己的类型

Constant_Base:
    tag = bytes(1)     # u1
    data = bytes()

这里使用括号来表示对应的字段的字节数量,每个tag都是1个字节,表示为tag(1),每个tag的值也对应着一种常量类型,看表:

taginfo
7Constant_Class_info { tag(1), name_index(2) }
9Constant_Fieldref_info { tag(1), class_index(2), name_and_type_index(2) }
10Constant_Methodref_info { tag(1), class_index(2), name_and_type_index(2) }
11Constant_InterfaceMethodref_info { tag(1), class_index(2), name_and_type_index(2) }
8Constant_String_info { tag(1), string_index(2) }
3Constant_Integer_info { tag(1), bytes(4) }
4Constant_Float_info { tag(1), bytes(4) }
5Constant_Long_info { tag(1), high_bytes(4), low_bytes(4) }
6Constant_Double_info { tag(1), high_bytes(4), low_bytes(4) }
12Constant_NameAndType_info { tag(1), name_index(2), descriptor_index(2) }
1Constant_Utf8_info { tag(1), length(2), bytes(length) }
15Constant_MethodHandle_info { tag(1), reference_kind(2), reference_index(2) }
16Constant_MethodType_info { tag(1), descriptor_index(2) }
18Constant_InvokeDynamic_info { tag(1), bootstrap_method_attr_index(2), name_and_type_index(2) }

大多常量类型的结构都是包含着索引,也就是各种XX_index,仅有少部分常量类型,比如类似Constant_Integer_info的几个表数值的常量,内容才是bytes,再如Constant_Utf8_info,内容也是bytes,代表着一个字符串,许多索引最终的指向都会来到这个常量类型。

(常量虽然因为类型不同长度不一,但因为大多以2byte表示索引的原因,长度一般是3byte/5byte,也并非毫无规律)
另外需要注意的是,根据《Java虚拟机规范》,Long和Double字面量会占两个表成员项的空间,意味着当常量池中计算这两种字面量时,索引会由n+1变成n+2了,因此会出现“跳索引”的现象。

根据这个表,我们可以肉眼解析一下class文件的内容,还是以最初的class文件举例,其内容开头是这样的:

cafe babe 0000 0034 003d 0a00 1100 2909
0010 002a 0900 1000 2b09 0010 002c 0900

解析出来如下表:

字段表长度原文表义
magic4 bytecafebabeclass文件头
minor_version2 byte0000副版本号0
major_version2 byte0034主版本号52,代表JDK1.8
constant_pool_count2 byte003d61,常量索引从1开始,即有60个常量
tag1byte0a10代表Constant_Methodref_info,当前常量索引为1
class_index2byte001117,指向索引为17的常量,且其常量类型为Constant_Class_info
name_and_type_index2byte002941,指向索引为41的常量,且其常量类型为Constant_NameAndType_info
tag1byte099代表**Constant_Fieldref_info **,当前常量索引为1
class_index2byte001016,指向索引为16的常量,且其常量类型为Constant_Class_info
name_and_type_index2byte002a42,指向索引为42的常量,且其常量类型为Constant_NameAndType_info
以此类推,直到常量池解析结束,应该有60个常量被解析出来。
类信息

当常量池阶段结束后,接下来是3*2byte的固定字段,分别是access_flags、this_class以及super_class,其中access_flags也有自己的一套规范:

转二进制
ACC_PUBLIC0x00010000 0000 0000 0001表示此类修饰符为public
ACC_FINAL0x00100000 0000 0001 0000表示此类修饰符final
ACC_SUPER0x00200000 0000 0010 0000JDK1.2之后均带此标志
ACC_INTERFACE0x02000000 0010 0000 0000表示此类为接口
ACC_ABSTRACT0x04000000 0100 0000 0000表示此类修饰符为abstract,不能被实例化
ACC_SYNTHETIC0x10000001 0000 0000 0000表示此类非Java源代码生成
ACC_ANNOTATION0x20000010 0000 0000 0000表示注解类
ACC_ENUM0x40000100 0000 0000 0000表示枚举类

看到1、2、4,第一感觉就是转成二进制应该会更明了。所以在上面的表里面直接转出来了。
这些类型相互之间可以组合,当然也有互斥,比如ACC_INTERFACE和ACC_ABSTRACT一般都是成对出现,组合起来都是位或的关系。
即:
一个具有ACC_PUBLIC和ACC_FINAL访问标志的类的最终访问标志值应该为:

0x0001 | 0x0010 = 0x0011

一个具有ACC_PUBLIC、ACC_INTERFACE和ACC_ABSTRACT访问标志的类的最终访问标志值应该为:

0x0001 | 0x0200 | 0x0400 = 0x0601

还是以之前的class文件为例,从access_flags开始读取到的6个byte为:

00 21 00 10 00 11

0x0021自然就是指0x0020|0x0001,也就是ACC_PUBLIC和ACC_SUPER,即此类是一个单纯以public为修饰符的类;
this_class与super_class不必多言,就是两个2byte的数值表示索引,这个索引会指向常量池。一个指向索引为0010=16的常量,一个指向0011=17的常量:

  #16 = Class              #55            // com/xter/design/builder/Point
  #17 = Class              #56            // java/lang/Object
  #55 = Utf8               com/xter/design/builder/Point
  #56 = Utf8               java/lang/Object

是的,这个类名就是Point,且直接继承自超类Object。

接口

类信息后,就是接口阶段,先读2byte得到接口数量,再通过数量去取各个接口的存储的索引值,这些索引必定指向一个类型为Constant_Class_info的常量
假如此阶段读取到的值为:

00 01 00 0D

即接口数量为0001=1个,此接口指向常量池中索引值为000D = 13的Constant_Class_info常量。

字段

接口阶段后为字段信息,同样是先读2byte得到字段数量,而字段与常量一样,是具有自己的结构的,但与常量不同的是,不会分出那么多类型,字段的结构是固定的:

Field_info{
    access_flags(2)
    name_index(2)
    descriptor_index(2)
    attributes_count(2)
    Attribute_info(attributes_count)
}

这里的access_flags与之前出现的类的访问标志类似,毕竟许多修饰符既可以修饰类本身,也可以修饰字段(变量)。有部分之前的表中没有出现的1、2、4,会在这里出现。看表:

转二进制
ACC_PUBLIC0x00010000 0000 0000 0001表示此字段修饰符为public
ACC_PRIVATE0x00020000 0000 0000 0010表示此字段修饰符为private
ACC_PROTECTED0x00040000 0000 0000 0100表示此字段修饰符为protected
ACC_STATIC0x00080000 0000 0000 1000表示此字段修饰符为static
ACC_FINAL0x00100000 0000 0001 0000表示此字段修饰符为final
ACC_VOLATILE0x00400000 0000 0100 0000表示此字段修饰符为volatile
ACC_TRANSIENT0x00800000 0000 1000 0000表示此字段修饰符为transient
ACC_SYNTHETIC0x10000001 0000 0000 0000表示此字段为编译器合成,非源代码产生
ACC_ENUM0x40000100 0000 0000 0000表示此字段为枚举值
name_indexdescriptor_index同样还是指向常量池的某个常量的索引。

字段比常量结构更复杂的是,其中还包含Attribute_info,即属性,属性既可包含在字段与方法之中,也能单独存在。

属性也拥有自己的结构Attribute_info,可以跳到下面的属性阶段查看。
依本文的例子Point.class为例,其中一个fied为:

Field # 0  Field_info -->
access_flags = 00 02
name_index = 18
descriptor_index = 19
attributes_count = 0
方法

字段后,为方法,同样先读2byte得到方法的数量,然后再看方法的结构,方法的结构与字段结构如出一辙:

Method_info{
    access_flags(2)
    name_index(2)
    descriptor_index(2)
    attributes_count(2)
    Attribute_info(attributes_count)
}

方法同样有修饰符,又与之前的字段修饰符也有一定差别。看表:

转二进制
ACC_PUBLIC0x00010000 0000 0000 0001表示此方法修饰符为public
ACC_PRIVATE0x00020000 0000 0000 0010表示此方法修饰符为private
ACC_PROTECTED0x00040000 0000 0000 0100表示此方法修饰符为protected
ACC_STATIC0x00080000 0000 0000 1000表示此方法修饰符为static
ACC_FINAL0x00100000 0000 0001 0000表示此方法修饰符为final
ACC_SYNCHRONIZED0x00200000 0000 0010 0000表示此方法修饰符为synchronize
ACC_BRIDGE0x00400000 0000 0100 0000表示此方法由编译器产生
ACC_VARARGS0x00800000 0000 1000 0000表示此方法有变长参数
ACC_NATIVE0x01000000 0001 0000 0000表示此方法非Java方法
ACC_ABSTRACT0x04000000 0100 0000 0000表示此方法修饰符abstract
ACC_STRICT0x08000000 1000 0000 0000表示此方法为strictfp,即FP-strict浮点模式
ACC_SYNTHETIC0x10000001 0000 0000 0000表示此字段为编译器合成,非源代码产生
与字段一样,name_indexdescriptor_index同样还是指向常量池的某个常量的索引。也同样有属性Attribute_info

依本文的例子Point.class为例,其中一个method为:

Method # 0  Method_info -->
access_flags = 00 01
name_index = 24
descriptor_index = 25
attributes_count = 1

	Attr # 0 Attribute_info -->
	attribute_name_index = 26
	attribute_length = 47
	attribute_content = 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 02 00 1B 00 00 00 06 00 01 00 00 00 03 00 1C 00 00 00 0C 00 01 00 00 00 05 00 1D 00 1E 00 00
属性

方法之后就是属性,先读2byte得到属性的数量,再看属性的结构:

Attribute_info{
    attribute_name_index(2)  		
    attribute_length(4)		
    attribute_content = bytes(attribute_length )
}

attribute_name_index表示一个索引,会直接指向一个Constant_Utf8_info的字符串,attribute_length表示接下来的attribute_content 的长度,依此长度可读取attribute_content的内容。
这个结构只是属性的共同结构,实际上由于attribute_content的内容不同,分为很多种类型的属性结构。不过由于种类过多,这里就不再深究的,其各种类型的属性结构与字段、方法等大同小异,只是嵌套会更加令人头疼而已。

比如在方法method_info经常出现的属性Code_atrribute,Code_atrribute是用来描述一个函数方法的,其结构是这样:

Code_attribute{
    attribute_name_index(2)  		
    attribute_length(4)	
    max_stack(2)
    max_locals(2)
    code_length(4)
    code(cdoe_length)
    exception_table_length
    exception_table(exception_table_length){
		start_pc(2)
		end_pc(2)
		handler_pc(2)
		catch_type(2)
	}
	attributes_count(2)
	attribute_info(attributes_count)
}

相比之下,字段、方法的结构比属性要简单许多,而这还只是一种属性的结构而已。在Java SE 8的规范中,属性的种类数量已经达到23个。

所以这里不再对属性作更详细的解析,只是把属性的内容得到就行。
依本文的例子Point.class为例,其中一个attribute为:

	Attr # 0 Attribute_info -->
	attribute_name_index = 39
	attribute_length = 2
	attribute_content = 00 28

这里解析出来的00 28,其实属于ConstantValue_attribute,这种属性的结构除了公共的两个字段attribute_name_index和attribute_length 外,仅仅只有一个constantvalue_index,也就是指向一个常量的索引0x0028=40。

至此,一个class文件就解析完毕,这里的Point.class源码为:

package com.xter.design.builder;

public class Point {

	private int x;
	private int y;
	private int z;
	private String desc;

	public void setX(int x) {
		this.x = x;
	}

	public void setY(int y) {
		this.y = y;
	}

	public void setZ(int z) {
		this.z = z;
	}

	public void setDesc(String desc) {
		this.desc = desc;
	}

	@Override
	public String toString() {
		return "Point{" +
				"x=" + x +
				", y=" + y +
				", z=" + z +
				", desc='" + desc + '\'' +
				'}';
	}
}

这里同样有一个用于解析class文件的py,当然没有javap好用,不过自己写一个解析流程的程序是有助于对class文件格式的理解的。

load_class.py

(之前好像因为放了PDF链接审核过不了了)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值