类文件结构(深入理解Java虚拟机笔记)

目录

魔术(0-3)、主(6-7)、次(4-5)版本号

常量池

访问标志

类索引,父类索引和接口索引集合

字段表集合

方法表集合

属性表集合

示例


将我们编写的程序编译成机器码已不再是唯一的选择,越来越多的语言选择了与操作系统和机器指令集无关的、平台中立的存储格式作为程序编译后的存储格式。ByteCode就是一种与平台无关的存储格式。然而产生ByteCode的java虚拟机可以编译任何复合规范的程序语言(java语言规范和java虚拟机规范),例如:Groovy,Scala语言等。

Class文件格式表:

U1,u2,u4,u8表示无符号的1字节、2字节、4字节、8字节数,可以用来描述数字、索引引用、数量值或utf-8编码的字符串。“_info”结尾的类型表示是一项复合数据类型。

魔术(0-3)、主(6-7)、次(4-5)版本号

package org.fenixsoft.clazz;

public class TestClass {

	private int m;

	public int inc() {
		return m + 1;
	}
}

常量池

主要存放2大类常量:字面量(Literal)和符号引用(Symantec Reference)(包括类和接口的全限定名、字段描述、方法描述)。

Class文件里没有内存布局信息,只有在运行期解析时(虚拟机加载Class文件的时候要动态链接),符号引用才会翻译到具体的内存地址。

首先是u2类型的一个值constant_pool_count[(8-9)-1=0x0016-0x0001=21],它代表常量池里项目的数量。其次才是如下结构的组合。

 

 Javap.exe命令可以查看具体的class文件信息  :

常量池有21项常量,其中有些常量会在方法表,属性表,字段表里使用。 

访问标志

常量池结束之后紧接着的2个字节存储access_flags,包括如下标志:

 

本例中,TestClass就是普通java类,acc_public和acc_super为真,其余为假(也就是0x0000),则access_flags的值为:acc_public | acc_super = 0x0021。 

下面的信息查看使用的javap版本是1.8.0_112

 Last modified 2018-10-12; size 393 bytes
  MD5 checksum 41e9c7e2e823b2a25d796bd022e828ed
  Compiled from "TestClass.java"
public class org.fenixsoft.clazz.TestClass
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // org/fenixsoft/clazz/TestClass.m:I
   #3 = Class              #20            // org/fenixsoft/clazz/TestClass
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/fenixsoft/clazz/TestClass;
  #14 = Utf8               inc
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               TestClass.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // m:I
  #20 = Utf8               org/fenixsoft/clazz/TestClass
  #21 = Utf8               java/lang/Object

常量池以字符串“java/lang/Object”结尾,所以下图中可以看出访问标志存储位置在00EF,00F0两处。

类索引,父类索引和接口索引集合

访问标志之后的字节存储类索引(u2类型,类,接口,枚举,注释)、父类索引(u2类型,Object类无父类为0x0000)、所有接口索引集合(一组u2类型,第一项u2类型数据表示接口的个数,为0x0000的话,则索引项到此为止再不占字节)。索引值是常量池中的tag值,也就是上面javap之后的输出以#开头的数字表示。

字段表集合

字段表集合(filed_info)用于描述声明的变量(类级static和实例级变量,不包括方法内声明的变量)。描述一个字段的信息有:字段的作用域(public,private,protected修饰符),是类级变量还是实例级变量(static修饰符),可变性(final),并发可见性(volatile修饰符,是否强制从主内存读写),是否可序列化(transient修饰符),字段数据类型(基本数据类型,对象,数组),字段名称。

字段表集合(filed_info)以一个fields_count(字段表的个数)的u2类型开始,接下来是一个个的字段表结构。

下面是字段表结构:

 其中access_flags类似于类的访问标志,是下列访问那标志的“|”。

以“_index”结尾的名称表示它的值在常量池。

字段表都包含的固定数据项目到descriptor_index为止就结束了,但是在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述0至多项额外的信息。字段表集合中不会列出超类或父接口中继承而来的字段,但有可能列表出原来Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字段码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

实例:

 

方法表集合

和字段表集合的存储方式几乎一致。

方法表集合(filed_info)以一个methods_count(字段表的个数)的u2类型开始,接下来是一个个的方法表结构。

下面是方法表结构:

 

Java方法中的代码存放在哪里?会在属性表集合里表述。这里要讲:存放在方法属性表集合(attribute_info)中的属性表有一个“Code”属性,用来存放编译器编译后的字节码指令。

实例 

attributes_count=0x0001说明了有一项属性表,attribute_name_index=0x0009索引值说明了对应的常量是“Code”。

当然了此属性表集合只是下面讲的一种。

属性表集合

Class文件,字段表,方法表,甚至是属性表都可以携带自己的属性表集合。在表示图中,属性表集合是指attribute_info类型的attributes项,紧接着attributes_count存放,attributes_count和attributes合起来称为属性表。

属性表集合里存放着一个个属性表结构的属性表项数据,Java虚拟机规范(java se 7)中定义了21中属性:具体见书,下图摘得部分:

 

属性表的名称就是attribute_name_index从常量池中索引到的CONSTANT_Utf8_info类型的值(字符串值)(例如:“Code”),属性表的值结构自己定义,不过按照如下格式(info自定义,u1类型是错误的表示,没有错误表示,u1和attribute_length(attribute_length个u1)一起表示info的长度):

 

Code属性

在Code属性表结构里,info属性项的长度是max_stack属性项以下的总长,也即Code属性表这个结构的长度减去6个字节。接口的方法和抽象方法的方法表的属性表集合里没有Code属性,若有,则如下结构:

max_stack代表了操作数栈(Operand Stacks)的最大深度。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Frame)中的操作数栈深度。

max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量表分配内存所使用的最小单位。对于byte,char,float,int,shot,boolean,reference和returnAddress等长度不超过32位的数据类型,每个局部变量占1个Slot,而double与long这两种64位的数据类型而需要2个Slot来存放。方法参数(包括实例方法中的隐藏参数“this”),显示异常处理器的参数(Exception Handler Parameter,即try-catch语句中catch块所定义的异常),方法体中定义的局部变量都需要使用局部表来存放。另外,并不是在方法中使用了多个局部变量,就把这些局部变量所占的Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所在的Slot就可以被其他局部变量所使用,编译器会根据变量的作用域来分类Slot并分配给各个变量使用,然后计算出max_locals的大小。

code是用于存储编译后生成的字节码指令的一系列字节流,每个指令就是一个u1类型的单字节,虚拟机读取到一个字节码指令就开始解释(按照各种平台的本地指令解释(jit))和执行。

code_length是u4类型的长度值,但虚拟机规范中限制了一个方法不允许超过65535条字节码指令,所以只是用了u2类型的长度。

在整个Class文件里,Code属性用于描述代码,其它的所有数据项目就都用于描述元数据(包括类、字段、方法定义及其它信息)。

Code之后是这个方法的显示异常处理表,异常表它包含4个字段: 

这些字段的含义为:如果字节码从第start_pc到end_pc行之间(不包含第end_pc)行出现了类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任何的异常情况都需要转向到handler_pc行行进行处理。异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。注:字节码的“行”是一种形象的描述,指的是字节码相对于方法体开始的偏移量,而不是Java源代码的行号。

SourceFile属性

         用于记录这生成这个Class文件的源码文件名称。

Exceptions属性

         这里的Exceptions属性是在方法表中与Code属性平级的一项属性,而不是Code属性表中的异常处理表。

LineNumberTable属性

         用于描述Java源代码行号与字节码行号(字节码偏移量)之间的对应关系。

LocalVariableTable属性

         用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。

 

LocalVaiableTypeTable属性

         在JDK1.5引入了泛型之后,与LocalVariableTable属性非常相似,仅仅是把记录字段描述符的descript_index替换成了字段的特征签名(Singnature,不是下面要讲的和Singnature属性平级的属性,它只是LocalVaiableTypeTable属性中的一项属性)。

Singnature属性

         在JDK1.5中大幅度增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Singature属性会为它记录泛型签名信息。用于记录泛型类型,java的泛型是伪泛型,在Code属性中存储的编译之后的字节码中,泛型信息(类型变量(Type Variables)或参数化类型(Parameterized Types))统统被擦除掉。Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。

示例

下面是一个整体查看class文件的例子,class文件二进制的,就不看了,我们看对class文件解析后的结构化视图。

public class TestClass<Tc> {
    private int m;
    private Tc t;

    public <Pm> int inc(Pm pt,Tc tc) {
        this.t = tc;
        int x;
        try {
            x = 1;
            return x;
        } catch (Exception e) {
            x = 2;
            return x;
        } finally {
            x = 3;
        }
    }

}

其中Constant常量池如

 

接下来我们看几个重要的截图,大家可以参照上面各个部分的讲解,想一想截图中的结构化描述信息应该处于class文件中的位置:

关于Singnature属性有:

Class相关的Singnature属性:

filed字段相关的Singnature属性: 

Method(inc方法)相关的Singnature属性:

方法表集合中的inc方法:

inc方法的Code属性:

接着放Code属性的code项:

0 aload_0
 1 aload_2
 2 putfield #2 <org/fenixsoft/clazz/TestClass.t>
 5 iconst_1
 6 istore_3
 7 iload_3
 8 istore 4
10 iconst_3
11 istore_3
12 iload 4
14 ireturn
15 astore 4
17 iconst_2
18 istore_3
19 iload_3
20 istore 5
22 iconst_3
23 istore_3
24 iload 5
26 ireturn
27 astore 6
29 iconst_3
30 istore_3
31 aload 6
33 athrow

 Code项之后是异常处理表:

 

         Code属性里也含有属性表结构,具体是LineNumberTable属性、LocalVariableTable属性、LocalVariableTypeTable属性,他们的结构化描述信息如下:

截图中cp_info #n指向常量池中的第n项常量。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值