文章目录
读完本文,你将会学到:
-
类中定义的field字段是如何在class文件中组织的
-
不同的数据类型在class文件中是如何表示的
-
static final类型的field字段的初始化赋值问题
注意:
私有字段
编译后,再用javap命令反编译,不在字段表集合中,这是什么原因呢?
1.概述
字段表集合
是指由若干个字段表(field_info
)组成的集合。对于在类中定义的若干个字段,经过JVM编译成class文件后,会将相应的字段信息组织到一个叫做字段表集合的结构中,字段表集合是一个类数组结构,如下图所示:
注意:这里所讲的字段是指在
类中定义的
静态或者非静态的变量,而不是在类中的方法内定义的
变量。请注意区别。
比如,如果某个类中定义了5个字段,那么,JVM在编译此类的时候,会生成5个字段表(field_info)信息,然后将字段表集合中的字段计数器的值设置成5,将5个字段表信息依次放置到字段计数器的后面。
2. 字段表集合在class文件中的位置
字段表集合紧跟在class文件的接口索引集合结构的后面,如下图所示:
3. Java中的一个Field字段应该包含那些信息?
字段表field_info结构体的定义
上面图中的数据类型和字段名称顺序反了
针对上述的字段表示,JVM虚拟机规范规定了field_info结构体来描述字段,其表示信息如下:
上面图中
加号
的数据类型和字段名称顺序反了,但是箭头
是对的
下面我将一一讲解FIeld_info的组成元素:访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表集合
4. field字段的访问标志
如上图所示定义的field_info结构体,field字段的访问标志(access_flags
)占有两个字节,共16位
,它能够表述的信息如下所示:
举例:如果我们在某个类中有定义field域:private static String str;
,那么在访问标志上,第15位ACC_PRIVATE
和第13位ACC_STATIC
标志位都应该为1。field域str的访问标志信息应该是如下所示:
如上图所示,str字段的访问标志的值为0x000A
,它由两个修饰符ACC_PRIVATE
和ACC_STATIC
组成。
根据给定的访问标志(access_flags
),我们可以通过以下运算来得到这个域有哪些修饰符:
上面列举的str
字段的访问标志的值为000A
,那么分别域上述的标志符的特征值取&
,结果为1的只有ACC_PRIVATE
和ACC_STATIC
,所以该字段的标志符只有有ACC_PRIVATE
和ACC_STATIC
。
5. 字段的字段名称表示和数据类型表示
field字段名称,我们定义了一个形如private static String str
的field字段,其中"str
"就是这个字段的名称。
用javap命令查看的反编译结果中,字段表集合中的
name_index
值被处理过了,直接显示最终值了,如果显示成#引用常量池
会更直观,可以参考 《图 7-1 实例解析》中的名称索引的值为"0x0005",即常量池中的#5,值为str
class文件对数据类型的表示如下图所示:
用javap命令查看的反编译结果中,字段表集合中的类型描述
descriptor_index
的值被处理过了,直接显示最终值了,如果显示成#引用常量池
会更直观,可以参考 《图 7-1 实例解析》中的描述索引的值为"0x0006",即常量池中的#6,值为 Ljava/lang/String
class文件将字段名称
和field字段的数据类型
表示作为字符串
存储在常量池
中。在field_info结构体中,紧接着访问标志
的,就是字段名称索引
和字段描述符索引
,它们分别占有两个字节,其内部存储的是指向了常量池中的某个常量池项
的索引,对应的常量池项中存储的字符串,分别表示该字段的名称和字段描述符。
6.属性表集合-----静态field字段的初始化
这个小章节的目的是介绍属性表集合,也就是某种条件下,才会有ConstantValue结构,但是《原文》描述有缺点,但是《原文》的图示是对的,可以参考,我又额外补充了《ConstantValue详解》
首先需要明确两个点:
(1) 字段中可以有自己的属性表;
(2) 并不是所有的字段都需要用到属性表;
attribute_info struct {
u2 `attribute_name_index`; //可用于字段表中的属性名称,见下表
u4 attribute_length;
u1 info // attribute_length个info
}
attribute_name_index
:指向一个常量池中CONSTANT_Utf8_info
的常量项,存放着这个属性的名称,比如常量池中的ConstantValue
,就是用在这个地方的,除了ConstantValue
之外,常见的属性如下表:
属性名称 | 含义 |
---|---|
ConstantValue | 该字段为final定义常量值 |
Signature | 支持泛型签名 |
Synthetic | 标识字段是否为编译器自动生成 |
RuntimeVisibleAnnotations | 标注运行时注解可见 |
RuntimeInvisibleAnnotations | 标注运行时注解不可见 |
可以参考 《图 7-1 实例解析》中的属性名称索引的值为"0x0007",即常量池中的#7,值为 ConstantValue
原文(有缺陷,建议跳过,但原文的图示是对的,可以参考)
在定义field字段的过程中,我们有时候会很自然地对field字段直接赋值,如下所示:
public static final int MAX=100;
public int count=10; //原文值为0,我觉得不好,无法区分编译后是否已经赋值
对于虚拟机而言,上述的两个field字段赋值的时机是不同的:
- 对于
非静态
(即无static修饰)的field字段的赋值将会出现在实例构造方法()中 - 对于
静态
的field字段,有两个选择:1、在静态构造方法<cinit>()
中进行;2 、使用ConstantValue
属性进行赋值
注意:下面这段原文有疑问,貌似不准确,
ConstantValue
是否存在和final
有关,和static
无关,建议跳过,因为作者举例的MAX之所以生成ConstantValue结构,根本原因还是有final修饰。
Sun javac编译器对于静态field字段的初始化赋值策略:
目前的Sun javac编译器的选择是:如果使用final
和static
同时修饰一个field字段,并且这个字段是基本类型
或者String
类型的,那么编译器在编译这个字段的时候,会在对应的field_info
结构体中增加一个ConstantValue
类型的结构体,在赋值的时候使用这个ConstantValue
进行赋值;如果该field字段并没有被final
修饰,或者不是基本类型或者String类型,那么将在类构造方法<cinit>()
中赋值。
对于上述的public static final init MAX=100;
javac编译器在编译此field字段构建field_info
结构体时,除了访问标志、名称索引、描述符索引外,会增加一个ConstantValue
类型的属性表。
ConstantValue详解
在前面提到过原文对ConstantValue
的解释不太准确,在JVM入坑(五) - .class文件结构分析(4):字段表集合 中提到ConstantValue比较详细的用法,直接借鉴。
ConstantValue属性的作用是通知JVM自动为final修饰变量(类变量和成员变量)生成ConstantValue属性
由此可以得出结论:
- 1、无论是类变量还是成员变量,被
final
修饰是ConstantValue属性的前提条件; - 2、只有字段的变量值为
基础数据类型
和String
类型才会有ConstantValue属性; - 3、无论字段如何被修饰,值只能是
字面量
,只要值是new的对象,一定没有ConstantValue属性;
import java.util.HashMap;
public class A {
final String str1 = "abc"; // ok,被final修饰+String类型声明+字面量(String类型)
final String str2 = new String("vvv"); // 与str2相似,但值是不是字面量(这里是new 出来的)
final HashMap map = new HashMap(); // 类型不是基础类型,也不是string类型
final int i1 = 101; // ok ,被final修饰+基础类型声明+字面量(基础类型)
static final int i2 = 102; // ok,与i1类似,但多了static修饰,说明static与final不冲突
static int i3 = 203; // 与i1相反,被static修饰,没被final修饰,说明与static无关
int i4 = 104; // 与i1类似,但没被final修饰,证明必须有final修饰
Integer i5 = Integer.valueOf(120); // Integer算对象类型,不是基础类型
int i6 = Integer.valueOf(106); // 值不是字面量
}
javap -v A
反编译,带ConstantValue
属性的已经被高亮显示:
final java.lang.String `str1`;
descriptor: Ljava/lang/String;
flags: ACC_FINAL
`ConstantValue: String abc`
final java.lang.String str2; //值是不是字面量(这里是new 出来的)
descriptor: Ljava/lang/String;
flags: ACC_FINAL
final java.util.HashMap map;
descriptor: Ljava/util/HashMap; //类型不是基础类型,也不是string类型
flags: ACC_FINAL
final int `i1`;
descriptor: I
flags: ACC_FINAL
`ConstantValue: int 101`
static final int `i2`;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
` ConstantValue: int 102`
static int i3; //没被final修饰
descriptor: I
flags: ACC_STATIC
int i4; //没被final修饰
descriptor: I
flags:
java.lang.Integer i5;
descriptor: Ljava/lang/Integer; // Integer算对象类型,不是基础类型
flags:
int i6; // 值不是字面量
descriptor: I
flags:
7.实例解析
定义如下一个简单的Simple
类,然后通过查看Simple.class
文件内容并结合javap -v Simple
生成的常量池内容,分析str field字段的结构:
package com.louis.jvm;
public class Simple {
private transient static final String str ="This is a test";
}
图 7-1 实例解析
注:
1. 字段计数器中的值为0x0001,表示这个类就定义了一个field字段
2. 字段的访问标志是0x009A,二进制是00000000 10011010,即第9、12、13、15位标志位为1,这个字段的标志符有:ACC_TRANSIENT、ACC_FINAL、ACC_STATIC、ACC_PRIVATE;
3. 名称索引中的值为0x0005,指向了常量池中的第5项,为“str”,表明这个field字段的名称是str;
4. 描述索引中的值为0x0006,指向了常量池中的第6项,为"Ljava/lang/String;",表明这个field字段的数据类型是java.lang.String类型;
5.属性表计数器中的值为0x0001,表明field_info还有一个属性表;
6.属性表名称索引中的值为0x0007,指向常量池中的第7项,为“ConstantValue”,表明这个属性表的名称是ConstantValue,即属性表的类型是ConstantValue类型的;
7.属性长度中的值为0x0002,因为此属性表是ConstantValue类型,它的值固定为2;
8.常量值索引 中的值为0x0008,指向了常量池中的第8项,为CONSTANT_String_info类型的项,表示“This is a test” 的常量。在对此field赋值时,会使用此常量对field赋值。
参考《Java虚拟机原理图解》1.4 class文件中的字段表集合–field字段在class文件中是怎样组织的
JVM入坑(五) - .class文件结构分析(4):字段表集合 ConstantValue详解