Java语言是跨平台的,其跨平台的基石是字节码,字节码按照java虚拟机规范的格式组成了class文件,并在虚拟机上运行。因此class文件的结构也是java跨平台很重要的一个基础。下面简单看看class文件的结构:
以上是class文件的基本结构,整个class文件分Magic,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods,Attributes几个部分。
先看一个class文件,简单分析一下其结构
Hello.java
以上是一个没有任何方法和变量的空类,这样,class里的内容就比较少,便于我们分析。先编译生成Hello.class
然后用od -t x1 Hello.class查看其内容
- 0000000 ca fe ba be 00 00 00 32 00 0d 0a 00 03 00 0a 07
- 0000020 00 0b 07 00 0c 01 00 06 3c 69 6e 69 74 3e 01 00
- 0000040 03 28 29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69
- 0000060 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 00 0a
- 0000100 53 6f 75 72 63 65 46 69 6c 65 01 00 0a 48 65 6c
- 0000120 6c 6f 2e 6a 61 76 61 0c 00 04 00 05 01 00 05 48
- 0000140 65 6c 6c 6f 01 00 10 6a 61 76 61 2f 6c 61 6e 67
- 0000160 2f 4f 62 6a 65 63 74 00 21 00 02 00 03 00 00 00
- 0000200 00 00 01 00 01 00 04 00 05 00 01 00 06 00 00 00
- 0000220 1d 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00
- 0000240 00 01 00 07 00 00 00 06 00 01 00 00 00 01 00 01
- 0000260 00 08 00 00 00 02 00 09
1、MagicNumber:cafe babe这个四个字节是用来标识class文件的,虚拟机加载class文件的时候会先检查这四个字节,如果不是cafe babe则虚拟机拒绝加载该文件。
下面我们来尝试修改该MagicNumber看看虚拟机的到底会不会加载我们的class文件。
用
vim -b Hello.class打开class文件,然后输入命令
“:%!xxd“
,class文件就会变成二进制文件,将cafebabe修改为aafebabe,
再“:%!xxd -r”转换回文本模式,":wq"存盘退出。
运行java Hello.class,此时会输出如下信息
- Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 2868820670 in class file Hello
输出的错误信息说:
2868820670(0xaafebabe)不是一个正确的mageic value。
ps: vim -b:是指以二进制方式打开文件
xxd:是输出文件的十六进制形式,xxd -r是将十六进制转换成二进制
修改cafe成aafe:文件转成十六进制后,光标移动到cafe的c上,按r进入替换模式,输入a,会将c替换成a,输入:wq保存即 可。
2、Version :0x0000 0032,前两个(0x0000)是次版本号,后两个(0x0032,表示十进制的50)是主版本号,因为java虚拟机规范一直在不断修改和完善,所以class文件会带上版本信息,表示其版本。这里的0x00000032,表示版本号为50.0,主版本号为50,次版本号为0. 如果版本号高于当前虚拟机支持的版本号,虚拟机也会拒绝加载该文件的。
下面我们动手修改一下class文件的版本,让其超出当前虚拟机支持的版本。又上面可知,我本机当前支持的版本为50.0,高出这个版本,虚拟机就会拒绝。这里我们将0x00000032改成0x00010032,再次java Hello。输出如下:
- Exception in thread "main" java.lang.UnsupportedClassVersionError: Hello : Unsupported major.minor version 50.1
以上的信息是说:不支持的major.minor 50.1,也就是当前的虚拟机不支持50.1版本,即是版本号为0x00010032的不被当前虚拟机支持。
3、constant_pool 常量池
既然是池子,肯定是拿来放东西的,名字叫常量池,那肯定是放常量的,那哪些常量是可以放的,又怎么放的呢。下面就讲解这两个问题
1)放什么
常量池中主要存放两类内容:字面常量和符号引用。
字面常量主要包含文本字符串,被声明为final的常量等。
符号引用:因为java在编译的时候没有进行连接这一步,所有的引用都是在加载到虚拟机里动态连接的,这就要求class文件里存放这些信息。主要有以下三类常量:
a) 类和接口的全限定名
b)字段的名称和描述符
c)方法的名称和描述符
2)怎么放
现在知道了常量池里要放什么东西,但是具体怎么放呢?
想一想,如果你有个箱子要放很多东西,第一件事情是什么呢?肯定是分类,对吧,把不同的东西按照类型分好,然后再决定怎么放。
那看看我们有哪些东西,怎么分类,怎么放。
首先,字面常量,想想java里的字面常量也就字符串,整形,长整型,浮点型,双精度浮点型,对于这些常量,我们只要存储它的类型、值的长度和具体的值就可以了。那么,这几个类型可以按照如下的形式来存储:
- CONSTANT{
- type_t type;//常量类型
- int length;//值的长度
- byte[] value;//常量的值
- }
这样,基本的字面常量的存储就搞定了。
但是再想想,上面的方案其实是可以优化的,对于整形,长整型,浮点型,双精度浮点型这些常量,它们的长度是固定的,也就是它们的类型决定了它们的长度,因此对于这些类型length这个字段是可以省略的。
然后,是符号引用,符号引用主要是存放了类和接口的引用,字段的信息,方法的信息。
类和接口在java里通过全限定名就可以确定,因此这里相当于存放一个字符串;
那么下面的结构就可以表示一个类了:
- CLASS_INFO{
- type_t type;//类型
- String fullname;//类或者接口的全限定名
- }
字段的信息包含字段所属的类,字段的类型,字段的名称,因此这里需要保存三个信息,但是在实际的class文件里,字段的类型和名称是放在一个结构里的。
方法的信息和字段的信息一样,但是方法会分成两类,一类是普通方法,一类是接口上的方法,可以通过类别来区分。
这样,字段和方法的信息就可以用以下结构来表示了:
- FILE_METHOD{
- type_t type;//字段或者方法
- CLASS_INFO *class;//指向class的引用
- NAME_AND_TYPE nameType;//字段的名称和类型,或者方法的名称和返回值
- }
还有一个结构NAME_AND_TYPE,这个结构是怎样的呢?由上面可知,这个结构是存放名称和类型的,也即是包含了两个字符串的一个组合,那么它的结构应该如下:
- NAME_AND_TYPE{
- type_t type;//类型
- String *name;//指向name的引用
- String *type;//指向type的引用
- }
还有一个结构是CONSTANT_Utf8_info,这个结构是存放utf-8字符串的,所有的字符串都是用这个结构封装的,String结构,Class结构里的字符串常量都是对CONSTANT_Utf8_info的一个引用,具体的值都会放在CONSTANT_utf8_info里。
它的结构如下:
- CONSTATN_Utf8_info{
- type_t type;//类型
- int length;//字符串占的字节数
- bytes[] value;//UTF-8编码的字符串
- }
最后,常量池的结构都弄明白了,下面来看看class文件分析一下:
除去前面的MagicNum和Version,接下来的两个字节0x000d表示常量池包含有多少个常量,这里显示有13个。
实际上是有12个,因为常量池里对常量的索引是从1开始的。当对第0个常量进行索引时,表示不指向任何常量。
接着就是第一个常量,首先是该常量的类型(字节码里用tag标识),0x0a表示tag=10,表示这个常量是类中方法的引用,那么接下来的信息就是方法所属的类和方法的名称和类型;接下来的两个字节0x0003表示指向第三个常量,那么第三个常量肯定是一个类;接着两个自己0x000a表示指向第10个常量,那么第十个常量肯定是一个NAME_AND_FIELD结构。
接着往下分析,其实很简单,常量分3类:
基本常量,包含Integer,Float,Long,Double,String,Class可以表示为:类型(1个字节)和值(长度由具体类型决定)
复合常量,包含Filed,Method,NameAndType可以表示为:类型(1个字节)索引1(2个字节)和索引2(2个字节)
Utf-8,包含类型(1个字节)长度(2个字节)和值(长度个字节)
那么按照这个结构把常量区重新调整一下:
- 0d 常量池包含的常量数目
- 0a 00 03 00 0a 方法,名称为第3个常量,返回值和类型为第10个常量
- 07 00 0b 类,名称为第11个常量Hello
- 07 00 0c 类,名称为第12个常量java/lang/Object
- 01 00 06 3c 69 6e 69 74 3e 字符串<init>
- 01 00 03 28 29 56 字符串()V
- 01 00 04 43 6f 64 65 字符串 Code
- 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 字符串LineNumberTable
- 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 字符串SourceFile
- 01 00 0a 48 65 6c 6c 6f 2e 6a 61 76 61 字符串Hello.java
- 0c 00 04 00 05 NAME_AND_FIELD ,name指向第4个常量<init>,field指向第5个()V
- 01 00 05 48 65 6c 6c 6f 字符串 Hello
- 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 字符串java/lang/Object
整个常量池的结构就非常清晰了
这里可以看到第一个常量是方法常量,它所属的类是java/lang/Object,它的名称和类型指向第十个常量,查看第十个常量,可以看到,其中name指向,类型指向()V,也就是第一个方法为void();这是编译器生成特殊方法,用来初始化用的。