Java 字节码结构

文章目录

字节码结构

区域 大小(字节) 说明
魔数 4 固定为 0xCAFEBABE
副版本号 2 Minor Version
主版本号 2 Majar Version
常量池计数器 2 常量池大小
常量池数据区 字面量和符号引用
访问标志 2 表示访问权限
类索引 2 确定当前类的全限定名
超类索引 2 确定当前类的超类全限定名
接口计数器 2 接口数量
接口表 接口索引数组集合
字段计数器 2 字段数量
字段表 字段数组
方法计数器 2 当前类的方法数量
方法表 方法数组集合
属性计数器 2 属性数量
属性表 属性数组集合

魔数

一个有效的 Java 字节码文件的前 4 个字节固定为:0XCAFEBABE,其他同样使用魔数的文件类型为:jpeg、png、gif 等。

JVM 用魔数来校验文件是否是一个有效且合法的字节码文件。

版本号

主版本号(M)和次版本号(m)限定了字节码文件支持的 JVM 版本。

常量池计数器和常量池

概述

常量池中的有效数据是从下标 1 开始的,0 下标保留,则常量池计数器也是从 1 开始的。

常量池中主要用于存放字面量和符号引用,其中:

字面量由文字字符串、final 常量值等构成;
符号引用则包括了类和接口的全限定名(Fully Qualified Name,FQN)、字段的名称和描述符(Descriptor),以及方法的名称和描述符。

假设有如下代码:

// Test.java
package com.example;

public class Test {
public final double j = 1.23;
public int i = 0;
private String s = "HelloWorld!";
public void hello() {
System.out.println(s);
}
}

首先将其编译为 Java 字节码:javac Test.java,然后使用 javap 工具查看其字节码内容:javap -v Test,截取 Constant pool 段如下:

 #1 = Methodref          #11.#26        // java/lang/Object."<init>":()V
#2 = Double 1.23d
#4 = Fieldref #10.#27 // com/example/Test.j:D
#5 = Fieldref #10.#28 // com/example/Test.i:I
#6 = String #29 // HelloWorld!
#7 = Fieldref #10.#30 // com/example/Test.s:Ljava/lang/String;
#8 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;
#9 = Methodref #33.#34 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = Class #35 // com/example/Test
#11 = Class #36 // java/lang/Object
#12 = Utf8 j
#13 = Utf8 D
#14 = Utf8 ConstantValue
#15 = Utf8 i
#16 = Utf8 I
#17 = Utf8 s
#18 = Utf8 Ljava/lang/String;
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 hello
#24 = Utf8 SourceFile
#25 = Utf8 Test.java
#26 = NameAndType #19:#20 // "<init>":()V
#27 = NameAndType #12:#13 // j:D
#28 = NameAndType #15:#16 // i:I
#29 = Utf8 HelloWorld!
#30 = NameAndType #17:#18 // s:Ljava/lang/String;
#31 = Class #37 // java/lang/System
#32 = NameAndType #38:#39 // out:Ljava/io/PrintStream;
#33 = Class #40 // java/io/PrintStream
#34 = NameAndType #41:#42 // println:(Ljava/lang/String;)V
#35 = Utf8 com/example/Test
#36 = Utf8 java/lang/Object
#37 = Utf8 java/lang/System
#38 = Utf8 out
#39 = Utf8 Ljava/io/PrintStream;
#40 = Utf8 java/io/PrintStream
#41 = Utf8 println
#42 = Utf8 (Ljava/lang/String;)V

第 #1 行表示存储的是一个方法引用,指向了 #11 和 #26,并用.连接它们;

找到 #11,它是一个类的符号引用,指向了 #36;找到 #26 ,它是一个字段或方法的符号引用,指向了 #19 和 #20,用:连接;

找到 #36,它是一个 UTF-8 编码的字符串:java/lang/Object;找到 #19 和 #20,它们都是 UTF-8 编码的字符串<init>:()V;

最终得到 #1 行的方法引用指向了java/lang/Object."<init>":()V,其他以此类推。

常量池中的表类型:

javap 结果 类型
Utf8 UTF-8 编码的字符串
Integer 整形字面值
Float 单精度浮点数字面值
Long 长整型字面值
Double 双精度副店数字面值
Class 类或接口的符号引用
String 字符串类型的字面值
Fieldref 字段的符号引用
Methodref 方法的符号引用
InterfaceMethodref 接口中方法的符号引用
NameAndType 字段或方法的部分符号引用
MethodHandle 方法句柄
MethodType 方法类型
InvokeDynamic 动态方法调用点

访问标志

常量池之后的两个字节存储的就是访问标志(access flags),主要用于表示该类或接口的访问权限。

有如下代码:

// Public.java
package com.example;
public class Public {}

编译为字节码后用 javap -v Public 查看,截取访问标志段如下:

flags: ACC_PUBLIC, ACC_SUPER

代表该类声明为了 public,可被包外的类访问。

标志名称 值 描述
ACC_PUBLIC 0x0001 声明为 public,可从包外访问
ACC_FINAL 0x0010 声明为 final,不允许派生
ACC_SUPER 0x0020 当用到 invokespecial 指令时,需要特殊处理的超类方法
ACC_INTERFACE 0x0200 标志定义的是接口而不是类
ACC_ABSTRACT 0x0400 声明为 abstract,不能被实例化
ACC_SYNTHETIC 0x1000 声明为 synthetic,标识并非由 Java 代码生成
ACC_ANNOTATION 0x2000 标识注解类型
ACC_ENUM 0x4000 标识枚举类型

类索引

当前类索引和超类索引都是指向常量池中的 Class 类型,Class 类型又指向 Utf8 类型,最终确定类的全限定名。

Java 语法规范规定只允许继承一个类,如果当前类没有显式继承任何超类时,则它的直接超类就是 Object,父类索引为 0,即指向了常量池中的下标 0 保留项。

接口计数器和接口表

接口计数器用于表示当前类或者接口的直接超类接口数量;接口表实际是一个数组集合,它包含了当前类或接口的直接超类在常量池中的索引集合,通过这个索引即可确定当前类或接口的超类接口全限定名。

字段计数器和字段表

字段计数器标识一个字节码文件中的字段总数,包括类变量和实例变量;字段表则是一个数组集合,其中的每个成员都表示了一个字段的完整信息。

有如下代码:

// Types.java
public class Types {

public static String s = "HelloWorld!";
public final double pi = 3.14;
public int i = 123;
protect Integer j = 456;
private Integer[] arr = new Integer[]{7, 8, 9};
}

编译为字节码后通过 javap 查看:javap -p -v Types,截取字段部分如下:

public static java.lang.String s;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC

public final double pi;
descriptor: D
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: double 3.14d

public int i;
descriptor: I
flags: ACC_PUBLIC

protected java.lang.Integer j;
descriptor: Ljava/lang/Integer;
flags: ACC_PROTECTED

private java.lang.Integer[] arr;
descriptor: [Ljava/lang/Integer;
flags: ACC_PRIVATE

其中每项都完整描述了该字段的相关信息,如描述符(descriptor)、访问标志(flags),甚至被声明为 final 的字段还携带了常量值(ConstantValue)。

字段描述符总结如下:

Descriptor 类型 含义
B byte 字节
C char 字符
D double 双精度浮点
F float 单精度浮点
I int 整形
J long 长整型
S short 短整型
Z boolean 布尔
L reference 对象类型
[ reference 数组类型

有如上总结表后,可得知:

上例中的 j,描述符为 Ljava/lang/Integer,即代表 Integer 对象;arr 描述符为 [Ljava/lang/Integer,即代表 Integer 数组。

方法计数器和方法表

方法计数器代表该类中的方法总数,仅包含该类或接口定义的方法,不包含继承而来的方法。

方法表则是一个数组集合,其中每项都表示了一个方法的完整信息。

有如下代码:

// Methods.java
public abstract class Methods {
public int intPublic() {
return 1;
}

private String strPrivate() {
return "HelloWorld!";
}

protected Object objectProtected() {
return new Object();
}

public static void voidStatic() {

}

public final void finalPublic() {

}

public synchronized void syncPublic() {

}

private void dynamicArgs(String first, Object... args) {

}

public abstract void abstractPublic();
}

编译为字节码后使用 javap 查看,并截取方法定义部分(仅保留描述符和访问标志)如下:

public Methods();
descriptor: ()V
flags: ACC_PUBLIC

public int intPublic();
descriptor: ()I
flags: ACC_PUBLIC

private java.lang.String strPrivate();
descriptor: ()Ljava/lang/String;
flags: ACC_PRIVATE

protected java.lang.Object objectProtected();
descriptor: ()Ljava/lang/Object;
flags: ACC_PROTECTED

public static void voidStatic();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC

public final void finalPublic();
descriptor: ()V
flags: ACC_PUBLIC, ACC_FINAL

public synchronized void syncPublic();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED

private void dynamicArgs(java.lang.String, java.lang.Object...);
descriptor: (Ljava/lang/String;[Ljava/lang/Object;)V
flags: ACC_PRIVATE, ACC_VARARGS

public abstract void abstractPublic();
descriptor: ()V
flags: ACC_PUBLIC, ACC_ABSTRACT

方法访问标志信息如下表:

标志名称 描述
ACC_PUBLIC 声明方法为 public
ACC_PRIVATE 声明方法为 private
ACC_PROTECTED 声明方法为 protected
ACC_STATIC 声明方法为类方法
ACC_FINAL 声明方法为 final
ACC_SYNCHRONIZED 声明方法为 synchronized
ACC_VARARGS 声明方法有变长参数
ACC_ABSTRACT 声明方法为 abstract

最后更新: