Java Class 文件结构
—— 从一个简单的接口去理解
话说书上说,要想学的深一点,必须深入到底层,了解虚拟机是如何工作的。
说实话自己写程序的时候遇到各种碰壁的情况。各种问题:
- 要加载个文件,classloader是怎么找路径的?
- 如何正确的定位到文件?
- 为什么我的类文件没有找到?
会有种种和虚拟机相关的东西,因为不懂而不知所措。即使一时找到了答案,解决了一个问题,但是我仍然、始终没有充足的信心说:就是这么一回事,从原理上讲就是这样。一个原因是我总是要花大把的时间去搜寻答案,而且找到了有时候也不明白,二是我不喜欢这种不知所以然的感觉,所以我想选择去了解虚拟机,从而在平时和她的“交往”中获得更多的默契。所以我想,是不是屏幕前的你也和我一样,想和她有更多的接触?
我不能十分确信的说这篇文章里所有的内容都是对的,但是它们都是能够说服我的,我也希望这些可以帮助你。而且自从接触,使用开源的软件后我注意到了分享的重要性。
总而言之:
这里我们要用一个非常简单的接口,编译成.class文件,并尝试讲清楚它的结构
首先,我们描述一下我们的Java文件(也就是我们要分析的Java class文件来源),然后大体上讲解一下 java class文件是怎么样存储的。最后详细的以16进制的形式分析清楚它的结构,所以本篇文章的目录是这样的:
- 我们分析的Java文件
- Java class文件存储结构
- 分析Java class文件
ref:The JavaTM Virtual Machine Specification 2nd edition
ref:《深入理解Java虚拟机》,周志明,P.136-170
我们分析的Java文件
首先我们创建一个非常简单的Java接口:
package com.uestc.event;
public interface Command {
public void execute();
}
然后我们将其编译成class文件:
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
Java class文件存储结构
这里粗略看一下,建议看完了下面的回过头来看一下。实际上,如下所示,一个.class文件就是一张表,只不过这张表是依次存放的:
Type | Name | Count | ||
u4 | magic | 1 | ||
u2 | minor_version | 1 | ||
u2 | major_version | 1 | ||
u2 | constant_pool_count | 1 | ||
cp_info | constant_pool | constant_pool_count - 1 | ||
u2 | access_flags | 1 | ||
u2 | this_class | 1 | ||
u2 | super_class | 1 | ||
u2 | interfaces_count | 1 | ||
u2 | interfaces | interfaces_count | ||
u2 | fields_count | 1 | ||
field_info | fields | fields_count | ||
u2 | methods_count | 1 | ||
method_info | methods | methods_count | ||
u2 | attributes_count | 1 | ||
attribute_info | attributes | attributes_count |
u4,u2代表4个,2个byte的长度的类型,其他诸如 field_info,method_info,attribute_info 是不一定长度的。因为像方法之类的一个 class 中可能有很多个。
magic, minor_version,major_version。总的来说,前四个(magic, minor_version,major_version)是用来确定版本号的,就是标示出是用的哪个版本的 jdk 编译的程序,比如你用 jdk 7 编译的程序无法用 jdk 6 运行应该就是从这里确定的。
constant_pool_count,constant_pool。随后的两个(constant_pool_count,constant_pool)是常量池,常量池里占到了文件的很大一部分地方,它和其他部分的关系也最为紧密,这里还不能一句话说明它的作用,举个例子,你比如说class文件的源文件(也就是.java)的文件名就是放到常量池里的,然后给它编个号,就可以方便的找打它,再比如虚拟机要确定当前类的父类,那么它就从常量池里去找这个父类,提前剧透一下,比如这里放的第四项常量 "#4=utf8 java/lang/object" 就是存放了当前类的父类名。其实常量池里就是放了很多的描述信息,你从这些信息的索引号(如这里的#4)去找到对应的项。
access_flags,this_class,super_class。它们一起描述了该类的可以访问性(access),即 public,private,protected,default。
interfaces_count,interfaces。它们一起描述了类实现的接口,而类可以实现多个接口,所以这里是多个。
fields_count,fields。它们描述了类所具有的域。我在这里的描述不够,接口嘛,借口了,没域。
methods_count,methods。描述了所有的方法,每个method其实又是另外一个”子表“了。
attributes_count,attributes。描述了所有的属性,也就是属性表合集。有9项属性:Code,ConstantValue,Deprecated,Exceptions,InnerClasses,LineNumberTable,LocalVariableTable,SourceFile,Synthetic。基本上每种的”子表“结构都不太一样。到这里class表也就结束了。
分析Java Class 文件
这里基本上按照 class 文件的大结构来分析。
1. magic, minor_version,major_version。
(1)前四个字节:cafe babe,魔数用于标记是class文件
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
(2)四个字节:版本号。前两个:此版本号。后两个:主版本号,每出一个版本主版本号加1,向后兼容(尊重历史吧……)
cafe babe 0000 0032 0009 0700 0201 0017636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
可以看到cafe babe,此版本号是0000,主版本号是0032(也就是3x16+2 = 50)是可以被java1.6或1.6以上执行的。
常见的java版本号对照表是:
major version number of the class file format being used.
J2SE 7 = 51 (0x33 hex),
J2SE 6.0 = 50 (0x32 hex),
J2SE 5.0 = 49 (0x31 hex),
JDK 1.4 = 48 (0x30 hex),
JDK 1.3 = 47 (0x2F hex),
JDK 1.2 = 46 (0x2E hex),
JDK 1.1 = 45 (0x2D hex).
2. constant_pool_count,constant_pool。
(3)2个字节:常量池入口,常量池的计数器,也即有多少个常量,如上面所示有9-1=8项常量,第0项空出来是为了提供“不引用任何一个常量”来提供索引值的。
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
下面就是进入去看常量池中的8个常量,常量都是:一个字节的tag + 其他来确定的,那么第一个就是tag了,tag确定了它是什么类型的常量。我们先来看看第 No.1 个常量:
(3.1)1个字节:类型tag,如这里的07,表示CONSTANT_Class_info,类或接口的符号引用,是由一个u1的tag + u2的name_index组成的(参考java常量池项目类型表)
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
(3.2)2个字节(其实这里的两个字节是因为前面07得到这是CONSTANT_Class_info项目类型,查此可以得到这里有两个字节的内容与其对应):偏移量name_index,这里为0002,也就是说指向常量池中的第二项常量。
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
(3.3)1个字节:查表可知是CONSTANT_Utf8_info,
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
(3.4)2个字节(还是其实是查CONSTANT_Utf8_info知道这里有三个字节):前2个字节表示length,即有多少个字节的。
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
这里表示接下来有23个字节(0x17)个字符,
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
这些换成char就是:com/uestc/event/Command
话说做笔记做到这作者才说有个叫javap的工具可以自动转,这……
比如说你用命令:javap -c Command.class,就可以得到它的内容:
使用:javap -verbose Command.class,就可以得到更多信息:
接下来与之对应的就是:07代表CONSTANT_Class_info,0004代表指向常量池第四项常量,标志位是01代表CONSTANT_Utf8_info,0010代表接下来16个字节是对应的内容,转换成char就是 java/lang/Object,
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
接下来与之对应的就是:01代表CONSTANT_Utf8_info,0007代表接下来7个字节是对应的内容,转换成char就是 execute
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
接下来与之对应的就是:01代表CONSTANT_Utf8_info,0003代表接下来3个字节是对应的内容,转换成char就是 ()V
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
接下来与之对应的就是:01代表CONSTANT_Utf8_info,000a代表接下来10个字节是对应的内容,转换成char就是 SourceFile
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
接下来与之对应的就是:01代表CONSTANT_Utf8_info,000c代表接下来12个字节是对应的内容,转换成char就是 Command.java
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
3. access_flags,this_class,super_class。
到这里常量池就没了,接下来是标志位:0601代表0x0001 + 0x0200 +0x0400,0001代表ACC_PUBLIC,0x0200代表ACC_INTERFACE,0x0400代表ACC_ABSTRACT,其实接口就是抽象的是吧:
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
然后是类索引,父类索引与接口索引集合,java里用这三项确定这个类的继承关系,前两个各自用一个u2类型,其实你也可以理解,因为java除了java.lang.Object外都是有且仅有一个父类。而接口索引是用u2类型的集合来表示的,因为java类可以实现多借口对吧。接下来的0001代表类索引为1,0003代表父类索引为3,0000代表接口索引集合大小为0。你看上面的:
也可以看到父类索引是#3,类索引为#1,
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
4. interfaces_count,interfaces。
因为这里没有实现接口,所以没有,就用0000表示。
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
5. fields_count,fields。
然后是字段表集合,用于描述接口或类中声明的变量。字段包括类级变量和实例级变量,它是由作用域(public……) + 实例变量或类变量标志(static) + 可变性(final) + 并发可见性(volatile) + 可否序列化(transient) + 类型(基本,对象,数组) + 名称,来表述的。如下所示依次,0000表示field——count,即有多少个字段,这里是0,话说抱歉啊,例子没选好,不全……
比如说按照书上讲的类里面定义了一个变量 private int m;那么蓝色的部分就是0001 0002 0005 0006。0001表示只有一个字段,0002表示private,0005表示索引为5,相应的在常量池里就会有#5 m了,0006指向常量池的字符串“I”(这里为啥我也还没搞明白)
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
6. methods_count,methods。
然后就是方法表集合,这里标志了方法。
- 0001表示方法的个数,这里只有一个
- 0401 = 0400(ACC_ABSTRACT) + 0001(ACC_PUBLIC)
- 0005表示名称索引值是#5 execute(见常量池)
- 0006表示函数的描述索引,其为常量池的第六个#6 ()V,()V表示函数返回为void,那要是返回是int型呢?那就是 ()I,要是带参数就是 (I)I,要是参数时数组就是 ([I)I,要是是二维数组就是 ([[I)I,其他类似
- 接下来的0000表示参数个数
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
7. attributes_count,attributes。
最后是属性表集合
- 0001:表示属性个数,一个
- 0007:属性种类由常量池第7项决定,可以看到是SourceFile
- 0000 0002:属性的长度,可以看到是2
- 0008:文件名指向第8个常量(不同的属性长度,不一样,这里是根据2.查表然后推出来的),也就是command.java
cafe babe 0000 0032 0009 0700 0201 0017
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 6407 0004 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0007
6578 6563 7574 6501 0003 2829 5601 000a
536f 7572 6365 4669 6c65 0100 0c43 6f6d
6d61 6e64 2e6a 6176 6106 0100 0100 0300
0000 0000 0104 0100 0500 0600 0000 0100
0700 0000 0200 08
汇总的:
cafe babe : u4 magic 1
0000 : u2 minor_version 1
0032 : u2 major_version 1
0009 : u2 constant_pool_count 1
常量池:
//---------------------------------------------------------------------------------
No.1:
07: CONSTANT_Class_info
00 02: name_index, 指向第二个常量
No.2:
01: CONSTANT_Utf8_info
00 17: 长度,23个字节u1
636f 6d2f 7565 7374 632f 6576 656e 742f
436f 6d6d 616e 64
No.3:
07: CONSTANT_Class_info
00 04: 第四个常量
No.4:
01: CONSTANT_Utf8_info
00 10: 长度,16个字节
6a 6176 612f 6c61 6e67 2f4f 626a 6563 74
No.5:
01: CONSTANT_Utf8_info
00 07: 长度,7个字节
6578 6563 7574 65
No.6:
01: CONSTANT_Utf8_info
00 03: 长度,3个字节
2829 56
No.7:
01: CONSTANT_Utf8_info
00 0a: 长度,10个字节
536f 7572 6365 4669 6c65
No.8:
01: CONSTANT_Utf8_info
00 0c: 长度,12个字节
43 6f6d 6d61 6e64 2e6a 6176 61
access_flags:
//---------------------------------------------------------------------------------
06 01: ACC_INTERFACE + ACC_ABSTRACT + ACC_PUBLIC
this_class:
//---------------------------------------------------------------------------------
00 01: 类索引,第一个常量
super_class:
//---------------------------------------------------------------------------------
00 03: 父类索引,第三个常量
interfaces_count:
//---------------------------------------------------------------------------------
00 00: 没有实现接口
fields_count:
//---------------------------------------------------------------------------------
00 00: 没有field
methods_count:
//---------------------------------------------------------------------------------
00 01: 一个方法
method_info:
access_flag:
04 01: ACC_ABSTRACT + ACC_PUBLIC
name_index:
00 05: 第五个常量
descriptor_index:
00 06: 第六个常量
attributes_count:
00 00:没有属性
attributes_count
//---------------------------------------------------------------------------------
00 01: 一个属性
No.1:
attribute_name_index
00 07: 第7个常量(查看常量池可以看到类型是SourceFile),而其对应以下的内容
attribute_length:
00 00 00 02: 属性长度,2个字节
sourcefile_index:
00 08:指向第八个常量