背景
在日常开发过程中,我们有时候需要查看 class 文件的内容。如果我们对 class 文件的结构有基本的了解,那么就会事半功倍。由于这个话题很大,而且我自己的水平也有限,所以每次只写一个很小的主题。本文的主题是理解 class 文件中方法的 access flags。
要点
目前(JDK26), class 文件中字段的 access flags一共支持 12 种类型( 如下图所示)。对每一种类型,本文都提供了对应的代码或者命令进行验证。

正文
The Java® Virtual Machine Specification 中的 4.6. Methods 小节详细介绍了 class 文件中 method_info 的结构,它的开头是这样的 ⬇️

u2 的含义
The Java® Virtual Machine Specification 中的 Chapter 4. The class File Format 开头解释了 u2 的含义 ⬇️ (如下图红色框所示)

u2 表示 2 byte 的无符号数
方法的 access flags
The Java® Virtual Machine Specification 中的 4.6. Methods 小节的开头提供了一个表格 ⬇️ (表格的名称是 Table 4.6-A. Method access and property flags)
| Flag Name | Value | Interpretation |
|---|---|---|
ACC_PUBLIC |
0x0001 | Declared public; may be accessed from outside its package. |
ACC_PRIVATE |
0x0002 | Declared private; accessible only within the defining class and other classes belonging to the same nest (§5.4.4). |
ACC_PROTECTED |
0x0004 | Declared protected; may be accessed within subclasses. |
ACC_STATIC |
0x0008 | Declared static. |
ACC_FINAL |
0x0010 | Declared final; must not be overridden (§5.4.5). |
ACC_SYNCHRONIZED |
0x0020 | Declared synchronized; 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 | Declared native; implemented in a language other than the Java programming language. |
ACC_ABSTRACT |
0x0400 | Declared abstract; no implementation is provided. |
ACC_STRICT |
0x0800 | In a class file whose major version number is at least 46 and at most 60: Declared strictfp. |
ACC_SYNTHETIC |
0x1000 | Declared synthetic; not present in the source code. |
我对 access flags 的理解是:方法中的某些信息可以用布尔值来表示(例如是否为 public,是否为 static,是否为 final),遇到这样的信息,可以考虑将其用 access flags 来表示。而像 类型 这种取值空间远非布尔值所能表示的信息,则应该通过其他属性来保存。
表格中一共列举了 12 种情况,我们分别来看。
前 6 种情况
请将以下代码保存为 Simple.java
java
// 请注意,代码中使用了 JDK 20 中新增的 api,如果 javac 的版本不够高,编译会失败
import java.lang.reflect.Method;
public class Simple {
private void method1() {
}
protected void method2() {
}
final void method3() {
}
synchronized void method4() {
}
public static void main(String[] args) {
for (Method method : Simple.class.getDeclaredMethods()) {
String message = String.format("method [%s] has access flags: %s",
method.getName(), method.accessFlags());
System.out.println(message);
}
}
}
使用如下的命令可以编译 Simple.java
bash
javac Simple.java
编译之后,当前目录会多出一个名为 Simple.class 的文件。下方的命令可以运行 Simple 类中的 main 方法
bash
java Simple
在我的电脑上,运行结果如下 ⬇️
text
method [main] has access flags: [PUBLIC, STATIC]
method [method1] has access flags: [PRIVATE]
method [method2] has access flags: [PROTECTED]
method [method3] has access flags: [FINAL]
method [method4] has access flags: [SYNCHRONIZED]
前 6 个 access flags 都出现了(其中 main 方法有两个 access flags 处于置位状态)
使用如下的命令可以查看 Simple.class 的详细内容。
bash
javap -v -p Simple
完整的结果略有点长,我把和本文相关的部分复制到下方了 ⬇️ (... 表示本文不关心的内容)
text
...
public class Simple
...
{
public Simple();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
private void method1();
descriptor: ()V
flags: (0x0002) ACC_PRIVATE
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
protected void method2();
descriptor: ()V
flags: (0x0004) ACC_PROTECTED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 10: 0
final void method3();
descriptor: ()V
flags: (0x0010) ACC_FINAL
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 13: 0
synchronized void method4();
descriptor: ()V
flags: (0x0020) ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 16: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
...
}
...
通过查看 javap 命令的输出,也可以验证,前 6 个 access flags 都出现了。

第 7 种情况: ACC_BRIDGE
我向 trae 询问了"什么情况下,javac 编译器会生成 bridge method?"这个问题。它举了些例子。在此基础上,我写了如下的代码 ⬇️ (请将其保存为 Case7.java)
java
// 请注意,代码中使用了 JDK 20 中新增的 api,如果 javac 的版本不够高,编译会失败
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class Case7 implements Callable<String> {
@Override
public String call() {
return "";
}
public static void main(String[] args) {
for (Method method : Case7.class.getDeclaredMethods()) {
String message = String.format("method [%s] has access flags: %s",
method.getName(), method.accessFlags());
System.out.println(message);
}
}
}
使用如下的命令可以编译 Case7.java
bash
javac -parameters Case7.java
编译之后,当前目录会多出一个名为 Case7.class 的文件。下方的命令可以运行 Case7 类中的 main 方法
bash
java Case7
在我的电脑上,运行结果如下 ⬇️
text
method [main] has access flags: [PUBLIC, STATIC]
method [call] has access flags: [PUBLIC]
method [call] has access flags: [PUBLIC, BRIDGE, SYNTHETIC]
ACC_BRIDGE 这个 access flag 出现了。使用如下的命令可以查看 Case7.class 的详细内容。
bash
javap -v -p Case7
完整的结果略有点长,我把和本文相关的部分复制到下方了 ⬇️ (... 表示本文不关心的内容)
text
...
public class Case7 extends java.lang.Object implements java.util.concurrent.Callable<java.lang.String>
...
{
public Case7();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
public java.lang.String call();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #7 // String
2: areturn
LineNumberTable:
line 10: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
...
public java.lang.Object call() throws java.lang.Exception;
descriptor: ()Ljava/lang/Object;
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #47 // Method call:()Ljava/lang/String;
4: areturn
LineNumberTable:
line 6: 0
Exceptions:
throws java.lang.Exception
}
...
基于 javap 命令提供的内容,可以手动反编译 Case7.class,我反编译的结果如下
java
// 以下内容是我手动反编译的结果,仅供参考
public class Case7 implements java.util.concurrent.Callable<String> {
public Case7() {
super();
}
// 这个 call 方法返回 String,另一个 call 方法返回 Object,在 java 代码中不允许这样的重载,但是在 class 文件中可以
public String call() {
return "";
}
public static void main(java.lang.String[] args) {
// 内容略
}
// 下面这个方法既是 bridge 方法,又是合成方法,在 java 代码里看不到它
// 这个 call 方法返回 Object,另一个 call 方法返回 String,在 java 代码中不允许这样的重载,但是在 class 文件中可以
public Object call() throws Exception {
return call();
}
}
第 8 种情况: ACC_VARARGS
支持变长参数的方法会用到这个 access flag。请将其保存为 Case8.java
java
// 请注意,代码中使用了 JDK 20 中新增的 api,如果 javac 的版本不够高,编译会失败
import java.lang.reflect.Method;
public class Case8 {
int calcSum(int... nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
return sum;
}
public static void main(String[] args) {
for (Method method : Case8.class.getDeclaredMethods()) {
String message = String.format("method [%s] has access flags: %s",
method.getName(), method.accessFlags());
System.out.println(message);
}
}
}
使用如下的命令可以编译 Case8.java
bash
javac -parameters Case8.java
编译之后,当前目录会多出一个名为 Case8.class 的文件。下方的命令可以运行 Case8 类中的 main 方法
bash
java Case8
在我的电脑上,运行结果如下 ⬇️
text
method [main] has access flags: [PUBLIC, STATIC]
method [calcSum] has access flags: [VARARGS]
ACC_VARARGS 这个 access flag 出现了。使用如下的命令可以查看 Case8.class 的详细内容。
bash
javap -v -p Case8
完整的结果略有点长,我把和本文相关的部分复制到下方了 ⬇️ (... 表示本文不关心的内容)
text
...
public class Case8
...
{
public Case8();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
...
int calcSum(int...);
descriptor: ([I)I
flags: (0x0080) ACC_VARARGS
Code:
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
...
}
...
通过查看 javap 命令的输出,也可以验证, ACC_VARARGS 这个 access flag 出现了。
第 9 种情况: ACC_NATIVE
Object 类 中就有 native 方法,例如 getClass() 方法 ⬇️

通过执行以下命令,我们就能看到 Object 类对应的 class 文件的详细内容 ⬇️
bash
javap -v -p 'java.lang.Object'
但是完整的结果比较长,这里我们只关心 getClass() 方法,所以可以用下方的命令
bash
javap -v -p 'java.lang.Object' | grep --after-context=2 'getClass()'
该命令的运行结果如下
text
public final native java.lang.Class<?> getClass();
descriptor: ()Ljava/lang/Class;
flags: (0x0111) ACC_PUBLIC, ACC_FINAL, ACC_NATIVE
容易验证, ACC_NATIVE 这个 access flag 出现了。
第 10 种情况: ACC_ABSTRACT
不难联想到,接口中定义的方法都是抽象的(这里不考虑接口中的默认方法,以及静态方法)。以 java.lang.Runnable 接口为例,其中的 run() 方法就是一个抽象方法。 通过执行以下命令,我们就能看到 java.lang.Runnable 接口对应的 class 文件的详细内容 ⬇️
bash
javap -v -p 'java.lang.Runnable'
运行结果如下(开头几行已略去)
text
public interface java.lang.Runnable
minor version: 0
major version: 65
flags: (0x0601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
this_class: #1 // java/lang/Runnable
super_class: #3 // java/lang/Object
interfaces: 0, fields: 0, methods: 1, attributes: 2
Constant pool:
#1 = Class #2 // java/lang/Runnable
#2 = Utf8 java/lang/Runnable
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 run
#6 = Utf8 ()V
#7 = Utf8 SourceFile
#8 = Utf8 Runnable.java
#9 = Utf8 RuntimeVisibleAnnotations
#10 = Utf8 Ljava/lang/FunctionalInterface;
{
public abstract void run();
descriptor: ()V
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "Runnable.java"
RuntimeVisibleAnnotations:
0: #10()
java.lang.FunctionalInterface
容易验证, ACC_ABSTRACT 这个 access flag 出现了。
第 11 种情况: ACC_STRICT
受 JEP 306: Restore Always-Strict Floating-Point Semantics 的影响,在高于或等于 17 的 JDK 版本中, java 代码中使用 strictfp 关键字变成了多余的行为。
参考 4.6. Methods 小节中 Table 4.6-A. Method access and property flags )的描述,要想看到 ACC_ABSTRACT 这个 access flag, class 文件的主版本号 Vmajor 需要满足 46≤Vmajor≤60 ⬇️

请将以下代码保存为 Case11.java
java
public class Case11 {
strictfp double calcSum(double a, double b) {
return a + b;
}
}
用以下命令可以将其编译为指定版本(即 JDK8)的 class 文件
bash
javac --source 8 --target 8 Case11.java
执行这个命令时,我看到了如下的警告
text
警告: [options] 未与 -source 8 一起设置引导类路径
警告: [options] 源值 8 已过时,将在未来发行版中删除
警告: [options] 目标值 8 已过时,将在未来发行版中删除
警告: [options] 要隐藏有关已过时选项的警告, 请使用 -Xlint:-options。
4 个警告
执行下方的命令可以看到 Case11.class 文件的详细内容
bash
javap -v -p Case11
运行结果如下(开头几行已略去)
text
public class Case11
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // Case11
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // Case11
#8 = Utf8 Case11
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 calcSum
#12 = Utf8 (DD)D
#13 = Utf8 SourceFile
#14 = Utf8 Case11.java
{
public Case11();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
strictfp double calcSum(double, double);
descriptor: (DD)D
flags: (0x0800) ACC_STRICT
Code:
stack=4, locals=5, args_size=3
0: dload_1
1: dload_3
2: dadd
3: dreturn
LineNumberTable:
line 4: 0
}
SourceFile: "Case11.java"
容易验证, ACC_STRICT 这个 access flag 出现了。
第 12 种情况: ACC_SYNTHETIC
请将以下代码保存为 Direction.java
java
// 请注意,代码中使用了 JDK 20 中新增的 api,如果 javac 的版本不够高,编译会失败
import java.lang.reflect.Method;
public enum Direction {
EAST,
WEST,
SOUTH,
NORTH;
public static void main(String[] args) {
for (Method method : Direction.class.getDeclaredMethods()) {
String message = String.format("method [%s] has access flags: %s",
method.getName(), method.accessFlags());
System.out.println(message);
}
}
}
使用如下的命令可以编译 Direction.java
bash
javac -parameters Direction.java
编译之后,当前目录会多出一个名为 Direction.class 的文件。下方的命令可以运行 Direction 类中的 main 方法
bash
java Direction
在我的电脑上,运行结果如下 ⬇️
text
method [main] has access flags: [PUBLIC, STATIC]
method [values] has access flags: [PUBLIC, STATIC]
method [valueOf] has access flags: [PUBLIC, STATIC]
method [$values] has access flags: [PRIVATE, STATIC, SYNTHETIC]
使用如下命令可以查看 Direction.class 的详细内容
bash
javap -v -p Direction
完整的结果略有点长,我把和本文相关的部分复制到下方了 ⬇️ (... 表示本文不关心的内容)
text
...
public final class Direction extends java.lang.Enum<Direction>
...
{
public static final Direction EAST;
descriptor: LDirection;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final Direction WEST;
descriptor: LDirection;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final Direction SOUTH;
descriptor: LDirection;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final Direction NORTH;
descriptor: LDirection;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
private static final Direction[] $VALUES;
descriptor: [LDirection;
flags: (0x101a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
public static Direction[] values();
descriptor: ()[LDirection;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
...
public static Direction valueOf(java.lang.String);
descriptor: (Ljava/lang/String;)LDirection;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
...
private Direction();
descriptor: (Ljava/lang/String;I)V
flags: (0x0002) ACC_PRIVATE
Code:
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
...
private static Direction[] $values();
descriptor: ()[LDirection;
flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
...
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
...
}
...
不难验证, ACC_SYNTHETIC 这个 access flag 出现了。至于 java 语言中,实现枚举类型的更多细节,可以参考我之前写的一篇文章:Java 浅析枚举的实现
参考资料
The Java® Virtual Machine Specification 中的
其他
"要点"中的那张图是怎么画出来的
我用了 python3 来画那张图。完整的代码如下
python3
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
n = 16
fig, ax = plt.subplots(figsize=(n, 2))
ax.set_xlim(0, n)
ax.set_ylim(0, 2)
ax.set_aspect('equal')
ax.axis('off')
def calc_2_pow(n):
result = 1
pow = 0
curr = 1
while curr != n:
pow += 1
curr *= 2
return pow
for i in range(n):
rect = patches.Rectangle((i, 0), 1, 1, linewidth=1, edgecolor='black', facecolor='lightgreen')
ax.add_patch(rect)
rect = patches.Rectangle((i, 1), 1, 1, linewidth=1, edgecolor='black', facecolor='lightgrey')
ax.add_patch(rect)
raw_data = {
0x0001: 'ACC_PUBLIC',
0x0002: 'ACC_PRIVATE',
0x0004: 'ACC_PROTECTED',
0x0008: 'ACC_STATIC',
0x0010: 'ACC_FINAL',
0x0020: 'ACC_SYNCHRONIZED',
0x0040: 'ACC_BRIDGE',
0x0080: 'ACC_VARARGS',
0x0100: 'ACC_NATIVE',
0x0400: 'ACC_ABSTRACT',
0x0800: 'ACC_STRICT',
0x1000: 'ACC_SYNTHETIC',
}
cell_text = {}
for (key, value) in raw_data.items():
pow = calc_2_pow(key)
col = n - 1 - pow
cell_text[(0, col)] = value
cell_text[(1, col)] = f"0x{key:04x}"
for (row, col), text in cell_text.items():
if row == 0:
fontsize = 6
else:
fontsize = 10
ax.text(col + 0.5, row + 0.5, text, ha='center', va='center', fontsize=fontsize, fontweight='bold')
plt.title('access flags')
plt.tight_layout()
plt.show()