深入理解JVM类文件格式(二)

简介: 深入理解JVM类文件格式

5.类索引、父类索引与接口索引集合


引用文章开头的ClassFile的数据结构,这三项定义为:

u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];


类索引和父类索引都是u2类型的数据,而接口索引首先给出了接口的数量,然后才是一个包含接口的数组。这三个值揭示了一个类的继承关系。

000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........|


接着前面的0x0021看,类索引为0x0003,指示常量池第3个常量,查上文可得#3 = Class #20 // com/shuqing28/TestClass,第3个常量又指向第20个常量,而第20个常量是一个CONSTANT_Utf8变量,其值为com/shuqing28/TestClass,表示类的全限定名字符串。

接下来的是0x0004是父类索引,指向常量池中第4个常量,即#4 = Class #21 // java/lang/Object, 又指向第21个变量,即java/lang/Object,我们知道Object是所有类的父类。
接下来的是
0x0000,可见TestClass没有实现任何接口。


6.字段表集合


字段表用于描述接口或者类中声明的变量。字段包括类级别的变量以及实例级的变量,但是不包括方法内的局部变量。一个Java字段可以包括以下信息:字段的作用域、是实例变量还是类变量、是否是final、并发可见性(volatile),是否可以被序列化(transient)、字段数据类型。下面是字段表具体结构:

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

再看access_flags可以取以下值:

标志名称

标志值

含义




ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; usable only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; never directly assigned to after object construction (JLS §17.5).
ACC_VOLATILE 0x0040 Declared volatile; cannot be cached.
ACC_TRANSIENT 0x0080 Declared transient; not written or read by a persistent object manager.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ENUM 0x4000 Declared as an element of an enum.

一般来说,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能存在一个,其它标志都按照Java语言本身的性质来。在access_flags标志的后面是两项索引值name_index,descriptor_index,两个都是指向常量池的索引,分别代表字段的简单名称以及字段和方法的描述符。这里我们梳理下简单名称、描述符以及全限定名这三个词对应的概念:
全限定名:前面提到的com/shuqing28/TestClass就是全限定名,它把java代码中所有的"."替换成了"/",一般使用";"结尾。
简单名称:不带类型和修饰的方法或者字段名,上文中的代码里就是"inc"和"m"
至于方法描述符,描述的是数据类型、方法的参数列表和返回值。我们知道在C++中重载函数时函数实际上是换了名字的,包含了函数的参数,例如add(int x, int y),在编译后可能是Add_Int_Int, 但是在Java中我们把基本数据类型都用一个大写字符来表示,而对象类则是使用L+对象的全限定名来表示。
描述符标识字符含义:

标识字符

含义



B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L Object, 例如 Ljava/lang/Object

对于数组,前面加[就行,如java.lang.String[][],表达为[[java/lang/String, int[]就被记录为[I
用描述符描述方法时,按照参数列表,返回值的顺序描述,参数列表还需要放在括号内。比如前文提及的"() V" 就表示一个参数为空,返回值为
void的方法,即代码中的void inc()方法。举个复杂点的, int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex),其描述符为([CII[CIII) I继续分析我们前文中提及的程序的二进制代码:

000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|
000000f0  05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|


上一小节我们分析到第一行的0x0000了,接下来的是0x01,这个值其实代表了字段表的个数,我们的代码里只包含一个字段。接下来的是0x0002,这个字段是access_flags标志,查询后可知为ACC_PRIVATE,再接下来是0x0005, 从常量表清单上可以查到是#5 = Utf8 m, 再接着是descriptor_index, 其值为0x0006,查一下常量池为#6 = Utf8 I,可知这一句为private int m;

一般来说,在decriptor_index后,还有个属性集合用于存储一些额外信息,而0x0000代表没有属性字段。
如果把m字段声明为
private static int m = 123;则可能多一个ConstantValue属性,指向常量值123。


7.方法表集合


方法表集合和字段表集合非常相似,结构也是:

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}


只不过在访问标志和属性表集合的可选项有所不同。例如access_flags有以下可选值:标志名称标志值含义




ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declaredprivate; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declaredprotected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declaredstatic.
ACC_FINAL 0x0010 Declaredfinal; must not be overridden
ACC_SYNCHRONIZED 0x0020 Declaredsynchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 A bridge method, generated by the compiler.
ACC_VARARGS 0x0080 Declared with variable number of arguments.
ACC_NATIVE 0x0100 Declarednative; implemented in a language other than Java.
ACC_ABSTRACT 0x0400 Declaredabstract; no implementation is provided.
ACC_STRICT 0x0800 Declaredstrictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

可以看出,方法里增加了像ACC_SYNCHRONIZED,ACC_NATIVE,ACC_STRICT, ACC_ABSTRACT, 分别对应着synchronizednativestrictfpabstract这些只能修饰方法的关键字。现在我们就可以继续分析我们程序的二进制代码了。

000000f0 05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|
00000100  09 00 00 00 2f 00 01 00  01 00 00 00 05 2a b7 00  |..../........*..|

上一小节我们刚刚分析到000000f0行的0x0000,接下来的是0x0002,代表有两个方法,接下来的几个字节是

  • 0x0001:访问标记是ACC_PUBLIC
  • 0x0007:名称索引指向第7个常量:<init>
  • 0x0008:描述符索引指向第8个常量:()V
  • 0x0001:属性有一个
  • 0x0009:属性指向第9个常量,Code

我们正好有疑问,方法定义有了,方法体在哪呢,答案就是上面分析的最后一个Code。下一节就说说属性表集合的各种可能。


8.属性表集合


属性表(attribute_info)在前面已经多次提及,Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景转有的信息。
属性表并没有严格限制顺序,只要不与已有属性名重复,任何人实现的编译器都可以添加自己定义的属性信息,以下是一些预定义的属性:
属性名称使用位置含义




SourceFile ClassFile 记录源文件的名称
InnerClasses ClassFile 内部类列表
EnclosingMethod ClassFile 内部类才有这个属性,用于标识这个类所在的外围方法
SourceDebugExtension ClassFile 用于存储额外的调试信息,JDK1.6中新增
BootstrapMethods ClassFile 用于保存invokeddynamic指令引用的引导方法限定符,JDK1.7中新增
ConstantValue field_info final关键字定义的常量值
Code method_info Java代码编译成的字节码指令
Exceptions method_info 方法抛出的异常
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations method_info 指明哪些参数是运行时可见的,哪些是运行时不可见的,JDK1.5中新增
AnnotationDefault method_info 记录注解类元素的默认值,JDK1.5中新增的
MethodParameters method_info 记录方法的参数信息,比如它们的名字,访问级别,JDK1.8新增
Synthetic ClassFile, field_info, method_info 表示方法或字段是编译器自动生成的
Deprecated ClassFile, field_info, method_info 被声明为deprecated的字段
Signature ClassFile, field_info, method_info 用于支持泛型情况下的方法签名,在Java语言中,如果任何类、接口、初始化方法或者成员的泛型签名包含了类型变量或者参数化类型,则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息。JDK1.5中新增
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations ClassFile, field_info, method_info 为动态注解提供支持,指明哪些是注解是运行时可见的,哪些是运行时不可见的,JDK1.5中新增
LineNumberTable Code Java源码的行号与字节码指令的对应关系
LocalVariableTable Code 方法的局部变量描述
LocalVariableTypeTable Code 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加,JDK1.5中新增
StackMapTable Code 供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作栈所需要的类型是否匹配,JDK1.6新增
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations ClassFile, field_info, method_info, Code 记录运行时类型上注解的可见性,也包括运行时类型参数的注解的可见性

下面具体说一说一些比较重要的属性:

Code属性

首先来看Code属性的结构:

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    { u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
  • attribute_name_index:占两个字节,指向CONSTANT_Utf8_info常量,表示属性名,这里固定是"Code"
  • attribute_length:属性值的长度,由于attribute_name_index和attribute_length占6个字节,所以attribute_length为属性表长度减6
  • max_statck: 操作数栈深度的最大值,在方法执行时,操作数栈不能超过这个值
  • max_locals:局部变量所需的存储空间。max_locals的单位是Slot,Slot是虚拟机为局部变量分配的最小单位,对于byte,char,float,int,short,boolean和returnAddress等长度不超过32位的数据类型,都只占一个slot,而double和long 这种64为的数据都是需要占用两个slot。方法参数(包括隐藏的this)、异常处理器的参数、方法体定义的局部变量都需要局部变量表来存放。但是max_locals并不是所有局部变量所占的slot之和,因为slot可以重用,当一个变量超出作用域了,该slot又会给别的局部变量使用,编译器会根据作用域计算max_locals。
  • code_length: 编译器编译后的字节码长度
  • code: 用于存储字节码指令的一系列字节流,每个指令是一个u1类型的单字节,当虚拟机读到该字节时,就可以知道是什么指令,知道是什么指令,就知道指令需要什么操作数,继续读就可以了,这里类似于汇编,u1的取值范围是0~255,可以表达256条指令。Java虚拟机规范中定义了约200条指令,参看Instructions。关于这部分内容以后再写博客介绍了。
  • exception_table_length:异常表的长度
  • exception_table:异常表对于Code来说并不是必须存在的,所以上述长度也可以为0,异常表有4个属性,代表着如果在start_pc到end_pc之间出现catch_type类型的异常,就跳转到handler_pc所指向的行处理。
Exceptions属性

Exceptions属性在方法表中与Code属性平级,注意和上面Code中的异常表不同,Exceptions属性的作用是列出方法可能抛出的异常,Exceptions属性表的结构:

Exceptions_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_exceptions;
    u2 exception_index_table[number_of_exceptions];
}


  • number_of_exceptions: 可能抛出的异常种类的个数
  • exception_index_table:指向常量池中CONSTANT_Class_info的索引
LineNumberTable属性

LineNumber用来记录Java源码与字节码行号之间的对应关系,我们在编译代码时也可以使用-g: none-g: line来取消生成这个属性,不过在调试代码时就看不到行号了,也无法打断点。LineNumberTable的数据结构如下:

LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    { u2 start_pc;
        u2 line_number;
    } line_number_table[line_number_table_length];
}


我们主要看line_number_table,start_pc是字节码行号,line_number是Java源码行号。

LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,我们在编译代码时也可以使用-g: none-g: vars来取消生成这个属性,但是如果取消的话,IDE会用arg0,arg1这样的参数来取代原有的参数名,导致调试时不清晰。

LocalVariableTable的数据结构如下:
LocalVariableTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_table_length;
    { u2 start_pc;
        u2 length;
        u2 name_index;
        u2 descriptor_index;
        u2 index;
    } local_variable_table[local_variable_table_length];
}


主要介绍local_variable_table:

  • start_pc和length:分别代表了这个局部变量的生命周期开始的字节码偏移量以及作用范围覆盖的长度
  • name_index和descriptor_index:分别指向代表局部变量名称和局部变量描述符的常量
  • index:是该局部变量在局部变量表中的slot位置,如果变量时double 或者long类型的,占用的slot为index和index+1两个。
ConstantValue属性

ConstantValue是一个定长属性,用来通知虚拟机为静态变量赋值,如果同时定义了int x=3;static int y=3;则虚拟机为x,y赋值的时机不同,对于x,是在实例构造器<init>中进行的,而static类型的变量,则会在类构造器<clinit>方法中或者使用ConstantValue属性。
目前javac编译器的规则是,如果同时有final和static修饰,则是使用ConstantValue属性,只有static时,并且变量类型是基本类型或者String时,就会在
<clinit>中进行初始化。

InnerClasses属性

如果类中定义了内部类,则会使用InnerClasses属性来记录内部类和宿主的关系。
InnerClasses的数据结构如下:

InnerClasses_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_classes; //记录有多少个内部类
    { u2 inner_class_info_index;
        u2 outer_class_info_index;
        u2 inner_name_index;
        u2 inner_class_access_flags;
    } classes[number_of_classes];
}


还是只看classes字段,inner_class_info_index指向内部类的符号引用,outer_class_info_index指向宿主类的符号引用,inner_name_index指向内部类的名称,如果是匿名内部类,则为0,inner_class_access_flags是内部类的访问标志,见下表:

标志名称

标志值

含义




ACC_PUBLIC 0x0001 Marked or implicitly public in source.
ACC_PRIVATE 0x0002 Marked private in source.
ACC_PROTECTED 0x0004 Marked protected in source.
ACC_STATIC 0x0008 Marked or implicitly static in source.
ACC_FINAL 0x0010 Marked final in source.
ACC_INTERFACE 0x0200 Was an interface in source.
ACC_ABSTRACT 0x0400 Marked or implicitly abstract in source.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

还有其它的一些属性,如果想了解,可以看一下参考资料。参考资料:

  1. Java Virtual Machine Specification
  2. Java虚拟机:JVM高级特性与最佳实践

相关文章
|
3天前
|
安全 Java
对 JVM 的类加载机制以及寻找字节码文件的“双亲委派模型”的理解
对 JVM 的类加载机制以及寻找字节码文件的“双亲委派模型”的理解
6 0
|
16天前
|
安全 Java
JVM的类的生命周期
JVM的类的生命周期
|
3天前
|
存储 前端开发 Java
深入浅出JVM(四)之类文件结构
深入浅出JVM(四)之类文件结构
深入浅出JVM(四)之类文件结构
|
4天前
|
存储 缓存 安全
JVM 类的加载篇
JVM 类的加载篇
9 0
|
18天前
|
Java
[JVM] Java类的加载过程
[JVM] Java类的加载过程
[JVM] Java类的加载过程
|
26天前
|
监控 安全 Java
JVM工作原理与实战(七):类的生命周期-初始化阶段
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了类的生命周期、类的初始化阶段等内容。
25 5
|
26天前
|
存储 安全 Java
JVM工作原理与实战(六):类的生命周期-连接阶段
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了类的生命周期、类的连接阶段等内容。
30 4
|
3天前
|
存储 缓存 算法
深入浅出JVM(二)之运行时数据区和内存溢出异常
深入浅出JVM(二)之运行时数据区和内存溢出异常
|
12天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
1天前
|
Java Linux Arthas
linux上如何排查JVM内存过高?
linux上如何排查JVM内存过高?
13 0