要真正掌握变量在字节码中的二进制表达,需直接解析.class文件的字段表(fields[])与常量池关系;字段集中存储于字段表区域,位于access_flags、this_class、super_class、接口列表之后,结构化为偏移固定、索引间接、类型显式的字节序列。

要真正掌握变量在字节码中的二进制表达,不能只看反编译后的Java源码,得直接拆解.class文件里字段表(field_info)和常量池的对应关系。变量不是“写出来就存在”,而是被结构化编码为偏移固定、索引间接、类型显式的字节序列。
看清字段在class文件里的物理位置
字段信息不散落在文件各处,而是集中存放在字段表(fields[])区域,位于访问标志(access_flags)、类索引(this_class)、父类索引(super_class)、接口列表之后。字段表前有一个
每个field_info结构固定包含三部分:
- access_flags(2字节):标识public、private、static、final等修饰符,按位掩码存储
- name_index(2字节):指向常量池中一个CONSTANT_Utf8_info条目,即变量名字符串
- descriptor_index(2字节):同样指向常量池,存放的是字段类型的描述符,如"I"表示int、"Ljava/lang/String;"表示String引用
注意:field_info本身不含字段值——实例变量值在对象实例化时分配在堆上;static变量初始值在准备阶段写入元空间,其“默认值”由JVM按类型规则赋予(如int为0,引用为null),显式赋值(如static int x = 5;)则编译进方法字节码中。
连通常量池理解变量名与类型的二进制编码
变量名和类型描述符都以UTF-8字符串形式存在常量池中,但不是明文存储。它们是CONSTANT_Utf8_info结构:
- 开头1字节tag(值为0x01)
- 接着2字节length(u2),表示后续字节数
- 最后length个字节才是真正的UTF-8编码内容(注意:Java常量池UTF-8支持modified UTF-8,对\0和surrogate pair有特殊处理)
例如字段private String name;,其name_index指向的常量池项内容是字节序列:0x01 0x00 0x04 0x6E 0x61 0x6D 0x65(即"tag=1, len=4, 'name'的UTF-8);descriptor_index则指向0x01 0x00 0x13 0x4C 0x6A 0x61 0x76 0x61 0x2F 0x6C 0x61 0x6E 0x67 0x2F 0x53 0x74 0x72 0x69 0x6E 0x67 0x3B,解码为"Ljava/lang/String;"。
动手验证:用javap + hex dump交叉比对
光读文档不如亲眼看到字节对齐。推荐组合使用:
- 先用
javap -v MyClass.class输出字段表和常量池索引关系 - 再用十六进制编辑器(如Notepad++ HEX-Editor插件或xxd)打开.class文件,跳转到字段表起始偏移(可通过javap中Constant pool大小估算)
- 对照javap输出的name_index值(比如#5),去常量池第5项(注意:常量池索引从1开始,且#0不占位)找对应UTF-8结构的起始位置
你会发现,字段表中两个2字节索引,像两把钥匙,分别打开常量池里两个字符串盒子——变量定义的本质,就是这种“索引+复用”的紧凑设计,而非重复存储文本。
区分实例变量与类变量的字节码痕迹
是否static,仅由access_flags中的标志位决定,字段表结构完全一致。但影响后续行为:
- static字段会在
方法中生成putstatic指令完成初始化 - 非static字段在
中通过putfield写入,且必须配合对象引用(this)操作数栈项 - 字段表本身不记录初始值,但若含constantvalue属性(仅适用于编译期常量,如
public static final int MAX = 100;),该属性会作为field_info的attributes附加结构出现,其中包含真正数值
也就是说,变量“有没有值”、“什么时候赋值”、“谁来赋值”,全靠字段标志、方法指令、属性表三者协同表达,而不是字段结构里塞一个值字段。











