class文件概述

将java代码编译后会产生class文件,并且一个clas文件会对应唯一一个java类或者接口。下面对一个通过一个简单的例子来简述一下class文件的结构。

java代码

public class JavaMethodAreaOOM{
    String str = "abc";
    int i = 1;
    static String str1 = "123";

    public static void main(String[] args) {
        int b = 1;
    }

    public void test(){
        System.out.println(str1);
    }
}

在java代码中我们定义了一些属性和方法。下面是编译生成的字节码文件

字节码文件

我们看到,源代码不长,但是生成的字节码文件内容却很多,下面就针对上面的例子进行分析。

字节码分析

  在字节码文件中只存储了两种类型的数据,一种是无符号数(u1,u2,u4,u8,u1表示一个字节,u2表示两个字节...),一种是表(表其实就是无符号的集合,不过表中的数据之间是有联系的,而且表中还可以存放表,表一般以_info结尾)。 字节码文件中数据存放顺序是按照下面表中的顺序存放的,比如文件的开始位置会存放4个字节的数据,我们称之为magic。所以,在进行字节码分析时,我们可以参考下面的表进行对比分析。

 

magic(u4)的翻译是魔术,但是这里是魔数。这个魔数的作用就是文件标识,用来标识这是一个字节码文件可以被JVM加载运行。查看字节码开始的4个字节,我们会发现是fecababe。所有的字节码文件开始都是这样的。

minor_version(u2)紧跟着魔数有两个字节(0000)表示次版本号,次版本一般是JDK的某个分支如JDK 1.1有个分支为JDK1.1.8,那么此版本号就为0x0003。这个次版本号不太重要。

major_version(u2)接下来的两个字节(0033)表示主版本号,这个主版本号很重要。我们经常会看到用低版本的java编译器编译出来的class文件无法在高版本的虚拟机上运行就是因为JVM会查看这个值。0x0033的十进制是51表示这个class文件只能在JDK1.7以上的虚拟机中运行(版本号对应的JDK这里可自行百度)。

constant_pool_count(u2):记录常量池中常量的个数,002C的十进制为44,表示在常量池中有44个常量。因为常量的池的大小不固定,如果没有这个数的话JVM在加载常量池不知道需要申请多大的空间才合适。

constant_pool(cp_info):这就是我们所谓的常量池,我们看到常量池使用表来表示的。其原因就是常量池中存放的大量的数据,我们用最大的基本类型u8也只能表示8个字节,所以只能用表来描述这段区域了。下面就是常量池中能放的内容:字面量和符号引用。字面量就是我们定义的字符串,final常量等,而符号引用包括类和接口的全限定名,方法名和描述符,字段名和描述符。

  对常量池中存放的常量都是以表的方式来描述的。在每个表的开始位置是个tag(u1),用来表示该常量是什么。我们可以对应下面的表来分析常量池中的数据。

  我们开始对常量池进行分析

  第一个常量0a00 0a00 1a,我们查表发现。OA是一个表示常量池中第一个常量是方法的符号引用,紧跟着我们看到000a 和001a。通过查表我们发现这是两个索引。第一个索引指向的是一个CONSTANT_Class_info这种表的数据位置,第二个索引指向CONSTANT_NameAndTyep_info这种表数据位置。简单来说,000A指向常量池中第10个常量,而001a指向第26个常量。下面是第26个常量中放的内容。我们查表可以得到07表示一个对类描述的表,0025又是一个索引,这个索引指向第37个常量。

  第二个常量08 001b,通过查表(08)我们发现,第二个常量是字符串类型。他的第二个索引是001b(27),指向第27个常量。至于第27个常量中具体放的什么,这里就不做分析了。

  总结一下,我们观察了常量池中的前两个常量,第一个是方法,第二个是字符串。但是我们发现方法处并未显示有具体的方法,字符串处并未看到具体的字符,放到全是索引。这就是class文件的一个特点,先说有这个东西,具体是是什么请通过后面的索引去查找。下面我们借助java中的工具看看常量池中内容。

图1.1

 通过工具我们可以直观的看到看到常量池中内容了。

  我们看到第一个常量是方法,这和我们分析的一样。常量中的数据是索引#10和#26。在#10处我们看到是一个Class常量,而Class中有个索引是#37.#37处的常量这里没有显示,但是通过后面的注释可以看到#37处放的是java/lang/Object字符串,存储类型是UTF8,表示类名。通过一步步查找我们可以知道,方法中的第一个常量是类名,表示这个方法属于哪个类,而“<init>”表明该方法的的名称,()V表示该方法的无参且返回值为void。那这个方法具体对应代码中的那个方法呢?其实就是我们从父类继承来的无参构造器。

  第二个常量是String类型,存储的数据是索引#27。而第27个常量处放的是Utf8类型,而内容为abc。这样就和我们的代码对应起来了,但是还一点就是这个字符串常量是被赋值给一个str属性的,这个str在哪里呢?

  第三个常量是Fieldref,我们看到它里面存的还是索引,而索引中的内容是UTF8类型的字符串。字符串的内容是 JavaMethodAreaOOM和str:Ljava/lang/String;这表示这个属性属于JavaMethodAreaOOM类的属性,而这个属性的类型是String类型。

  第四个常量也是Fieldref,这就是我代码中的int类型了。索引中存储的是JavaMethodAreaOOM和i:I,前者表示所属类,后者是名字和类型。i就是变量名而I就是int类型了。

 常量池总结

量池中大的分类就是字面的量和符号引用。细化后就是字符串常量,索引值,类名,方法名,属性名,属性的类型,方法的参数,方法的返回值。而方法体以及一些其他属性的初始化操作(int=1)会放在class文件的代码区。

访问标志

常量池之后是访问标志(两个字节)了,这个访问标志了这个类的访问信息。包括是接口还是类,是否为public。下面是对访问标志的描述。

 类索引,父类索引,接口索引

按照最开始的那张表来看,字节码文件中访问标志位后面的应该是类关系,主要记录该类叫啥,该类的父类是谁,该类实现的接口的数量以及具体的接口。下面是具体的内容。

  对照表6-6我们知道0009是一个u2类,转为十进制就是9,这个9其实是个索引,而这个索引指向的位置是常量区。通过查看图1.1发现,9的位置又是一个索引指向#36,而36的位置放的是字符串 JavaMethodAreaOOM,这就是这个类的类名了。000a十进制位10,而10的位置放的是索引37,37的位置的放的是java/lang/Object。0000表示该类实现的接口数量为0。

  通过以上分析,我们即使不看源代码,通过字节码文件就可以看出该类的关系图了。从这里我们也发现了索引存储的魅力(伟大的复用性)。

域描述

  域描述就是描述接口或类中定义的变量。对域的描述包括作用域(public,protected,private),是实例变量还是类变量(static),可变性(final),并发可见性(volatile),是否能被序列化(transient),字段类型(基本类型,对象,数组,),字段名称等等。这些内容都要在域描述中体现出来。 

方法描述

  对于方法的描述和对域描述的方式几乎是一致的,都是采用下图中的描述方式。

access_flags:访问标志

name_index:方法名索引

descriptor_index:参数以及返回值类型索引

attribute_info:对于方法而言这里放的就是Code(代码了)

属性表

字节码文件最后放的就是属性表了。在域描述和方法描述中都提到了属性表。这里不做分析,我们只关心其中方法中的code内容,code从字面上看就是代码的意思。code中放的就是每个方法体被编译后生成的java指令。下图就是每个code的具体内容了。

关于字节码指令的解读放在下一章,这里我们只需要注意到test()方法是一个无参数函数,但是其code属性中显示参数的个数(args_size=1)。这个1表示的其实是this这个参数。我们之所以能在方法内使用this就是因为jvm在加载类时会为每个方法自动传入一个this参数。

posted @ 2016-08-31 11:24  被罚站的树  阅读(1125)  评论(0编辑  收藏  举报