1、字段表
字段表紧随在接口表索引之后,字段表包含访问标记、字段名索引、描述符索引、属性表,其中属性表包含属性计数器与属性集合
以这段代码为例:
package com.yang.testField;
public class Main {
private volatile int a = 1;
public static final String b = "abc";
}
16进制数据如下图所示:
可以看得出,字段计数为0x0002,因为有2个字段,a和b。
字段a的访问标记是是0x0042,用这个值与标识符的特征值取与,如果结果为1,则表示该字段拥有响应的标识符。字段标识符如下所示:
访问标记名 |
十六进制值 |
描述 |
ACC_PUBLIC |
0x0001 |
public |
ACC_PRIVATE |
0x0002 |
private |
ACC_PROTECTED |
0x0004 |
protected |
ACC_STATIC |
0x0008 |
static |
ACC_FINAL |
0x0010 |
final |
ACC_VOLATILE |
0x0040 |
volatile |
ACC_TRANSIENT |
0x0080 |
transient,被transient 修饰的字段默认不会被序列化 |
ACC_SYNTHETIC |
0x1000 |
表示这个字段是由编译器自动生成,而不是用户代码编译产生 |
ACC_ENUM |
0x4000 |
枚举 |
这里我们可以得出,a的访问标记有ACC_PRIVATE与ACC_VOLATILE。
a的名称索引为0x0005,我们看一下常量池:
可以得出第一个字段的名称索引指向常量池中第5个常量项,即“a”。
a的描述符索引为0x0006,即常量池中的“I”,完成的字段类型与描述符的对照表如下:
描述符 |
类型 |
B |
byte |
C |
char |
D |
double |
F |
float |
I |
int |
J |
long |
S |
short |
Z |
bool |
LClassName ; |
引用类型,"L" + 对象类型的全限定名 + ";" |
[ |
一维数组,N维数组就有N个连续的[ |
接下来是a的属性计数器,对应的值为0x0000,代表a没有属性表。
贴一下b字段表中的属性表:
b的属性计数器为0x0001,代表着有属性表,属性表中只有一个元素,为0x0009,常量池中显示为ConstantValue,说明
该属性是ConstantValue类型的,属性长度为2,属性值索引为0x000A,即找到常量池中的#11,再找到#21,原来是个字符串"abc"。
为什么int a没有属性表,而static final b却有属性表?这要从字段的赋值策略说起:
对于一个实例字段,比如这里的a,赋值阶段发生在对象实例的构造方法中,即<init>;
对于一个非final的静态字段,赋初始值会发生在解析阶段,而赋用户指定的值,会发生在初始化阶段,在类构造器方法中完成,即<clinit>。
对于一个final的静态字段,且是基本类型或者是String类型,在编译期间就给该变量赋予用户指定的值,并在常量池中形成一个ConstantValue类型的属性,属性值就是常量的值。如果是除去String类型以外的引用类型,那么就是在初始化阶段完成赋值操作。
下面以一个例子说明:
package com.yang.testField;
public class Main {
private volatile int a = 1;
public static final String b = "abc";
public static String c="def";
public static Thread d=new Thread();
}
<init>方法内的情况:
这里面完成的是对实例变量的赋值操作。
<clinit>方法内的情况:
这里面完成的是对普通静态变量c与非String的引用类型变量d的赋值操作。
更多关于对<init>与<clinit>方法的理解,可以参考这篇文章java执行顺序之深入理解clinit和init
2、方法表
紧接着字段表的是方法表,方法表和字段表类似,方法表包含方法计数、访问标记、名称索引、描述符索引、属性表,其中属性表也是包含属性计数与属性集合。
方法计数、名称索引这边就不再说明了。
方法的访问标记有:
方法访问标记 |
特征值 |
描述 |
ACC_PUBLIC |
0x0001 |
public |
ACC_PRIVATE |
0x0002 |
private |
ACC_PROTECTED |
0x0004 |
protected |
ACC_STATIC |
0x0008 |
static |
ACC_FINAL |
0x0010 |
final |
ACC_SYNCHRONIZED |
0x0020 |
synchronized |
ACC_BRIDGE |
0x0040 |
bridge 方法, 由编译器生成 |
ACC_VARARGS |
0x0080 |
方法包含可变长度参数,比如 String... args |
ACC_NATIVE |
0x0100 |
native |
ACC_ABSTRACT |
0x0400 |
abstract |
ACC_STRICT |
0x0800 |
声明为 strictfp,表示使用 IEEE-754 规范的精确浮点数,极少使用 |
ACC_SYNTHETIC |
0x1000 |
表示这个方法是由编译器自动生成,而不是用户代码编译产生 |
这里有一个简单的例子:
package com.yang.testMethod;
public class Main {
public Main() {
}
private int getInt(int k) {
return k;
}
public static Thread getThread(int i, double d, Runnable runnable) {
System.out.println(i * d);
return new Thread(runnable);
}
}
构造方法的描述符为()V
getInt方法的描述符为(I)I
getThread方法的描述符为(IDLjava/lang/Runnable;)Ljava/lang/Thread;
从这里,我们可以看得出,方法描述符的组织方式是这样子的:(参数列表内字段的描述符)返回值的描述符
接下来讨论方法的属性表,前面说过了,属性表包含属性计数与属性集合,属性集合又包含属性名称索引+属性长度+属性值。
属性表内最主要的属性就是Code属性了,Code属性内有几个比较重要的东西:字节码、LineNumberTable行号表、LocalVariableTable局部变量表、ExceptionTable异常表
用一下的代码为例:
public static Thread getThread(int i, double d, Runnable runnable) {
try {
System.out.println(i * d);
}catch (Exception e){
return null;
}
return new Thread(runnable);
}
字节码是class文件中最重要的东西了,jvm主要就是抽取字节码,然后去执行。
LineNumberTable内维护这java源码与字节码之间的对应关系:
LocalVariableTable内记录着局部变量描述:
关于局部变量表的详细内容,可以参考我的另外一篇文章虚拟机栈的五脏六腑。
ExceptionTable会告诉虚拟机异常的处理逻辑,比如下图的异常表,说明如果字节码从第0行到第10行出现了type类型的异常,那么将会跳转到第13行的字节码进行处理。