前言:前段时间读《深入java虚拟机》介绍到class文件的时候,由于理论知识较多,人总感觉疲惫不堪,就泛泛阅读了一下。在工作中使用起来知识点知道,但是总是需要查阅各种资料。今天有时间,继续整理常量池后面的相关知识。
public class Sample {
public String m1;
public String m2;
public Object [] arr;
public static void main(String[] args) {
Sample sample = new Sample();
sample.m1="22";
sample.arr=new Object[12];
System.out.println(sample.m1);
}
}
Last modified 2023-6-2; size 708 bytes
MD5 checksum fc8bb4833223a10b68449d42080b1695
Compiled from "Sample.java"
public class com.company.jvm.Sample
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#29 // java/lang/Object."":()V
#2 = Class #30 // com/company/jvm/Sample
#3 = Methodref #2.#29 // com/company/jvm/Sample."":()V
#4 = String #31 // 22
#5 = Fieldref #2.#32 // com/company/jvm/Sample.m1:Ljava/lang/String;
#6 = Class #33 // java/lang/Object
#7 = Fieldref #2.#34 // com/company/jvm/Sample.arr:[Ljava/lang/Object;
#8 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream;
#9 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = Utf8 m1
#11 = Utf8 Ljava/lang/String;
#12 = Utf8 m2
#13 = Utf8 arr
#14 = Utf8 [Ljava/lang/Object;
#15 = Utf8
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcom/company/jvm/Sample;
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 args
#25 = Utf8 [Ljava/lang/String;
#26 = Utf8 sample
#27 = Utf8 SourceFile
#28 = Utf8 Sample.java
#29 = NameAndType #15:#16 // "":()V
#30 = Utf8 com/company/jvm/Sample
#31 = Utf8 22
#32 = NameAndType #10:#11 // m1:Ljava/lang/String;
#33 = Utf8 java/lang/Object
#34 = NameAndType #13:#14 // arr:[Ljava/lang/Object;
#35 = Class #39 // java/lang/System
#36 = NameAndType #40:#41 // out:Ljava/io/PrintStream;
#37 = Class #42 // java/io/PrintStream
#38 = NameAndType #43:#44 // println:(Ljava/lang/String;)V
#39 = Utf8 java/lang/System
#40 = Utf8 out
#41 = Utf8 Ljava/io/PrintStream;
#42 = Utf8 java/io/PrintStream
#43 = Utf8 println
#44 = Utf8 (Ljava/lang/String;)V
{
public java.lang.String m1;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public java.lang.String m2;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public java.lang.Object[] arr;
descriptor: [Ljava/lang/Object;
flags: ACC_PUBLIC
public com.company.jvm.Sample();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/company/jvm/Sample;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/company/jvm/Sample
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: aload_1
9: ldc #4 // String 22
11: putfield #5 // Field m1:Ljava/lang/String;
14: aload_1
15: bipush 12
17: anewarray #6 // class java/lang/Object
20: putfield #7 // Field arr:[Ljava/lang/Object;
23: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_1
27: getfield #5 // Field m1:Ljava/lang/String;
30: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 14
line 12: 23
line 13: 33
LocalVariableTable:
Start Length Slot Name Signature
0 34 0 args [Ljava/lang/String;
8 26 1 sample Lcom/company/jvm/Sample;
}
SourceFile: "Sample.java"
class文件中,我们可以通过背或记也好,或者通过查阅对照表。可以将常量池中的数据整理出来。常量池的数据,之后又是什么呢?紧接着的就是访问标志。
标志名称 | 标志值 | 标志意义 |
---|---|---|
ACC_PUBLIC | 0X0001 | 是否为public类型 |
ACC_FINAL | 0X0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 在jdk1.0.2之后编译出来的类的这个标志都为真 |
ACC_INTERFACE | 0x0200 | 标识是一个接口 |
ACC_ABSTRACT | 0x4000 | 是否为abstract类型,如为真,其他类型均为假,如INTERFACE |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
下面是常量池之后的class文件截取的部分:
00 21
访问标志,占用空间:U2,这里占用了十六进制的4个位,则是0x0021。这里我啰嗦一下,一个字节为8个位,对应到十六机制来说【两个位】代表一个字节。eg:0xF标识二进制的0000 1111,去除高位的0,就是1111。
在表格中我们说过,ACC_SUPER标志在jdk1.0.2之后的版本其值都为真,则是0x0020,说明其访问标志ACC_PUBLIC为真!0x0001|0x0020=0x0021
访问标志结束之后,就来到了我们所声明的类例,如下伪代码
public class dog extend cat implement animal{
}
00 02 00 06 00 00
__类索引、父类索引、接口索引占用的内存均为u2。
u2 | 索引 | 说明 |
---|---|---|
00 02 | #2 | 代表当前类的索引,通过查找为com/company/jvm/Sample |
00 08 | #8 | 代表当前父类索引,通过查找为Object |
00 00 | 0 | 代表当前文件没有接口 |
通过字面意,就能得知这里将要介绍的是类或接口成员字段。
//这里写个伪代码
public final static int AGE=10;
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | filed_count | 1 | 字段数量 |
u2 | access_flag | 1 | 访问标志 |
u2 | name_index | 1 | 名称索引 |
u2 | descriptor_index | 1 | 类型索引 |
u2 | attribute_count | 1 | 属性计数器 |
u2 | attributes | attribute_count | 属性值集合 |
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否为公开 |
ACC_PRIVATE | 0x0002 | 字段是否为私有 |
ACC_PROTECTED | 0x0004 | 字段是否为保护 |
ACC_STATIC | 0x0008 | 字段是否为静态 |
ACC_FINAL | 0x0010 | 字段是否为Final |
ACC_VOLATILE | 0x0040 | 字段是否在并发时可见 |
ACC_TRANSIENT | 0x0080 | 字段是否序列化 |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自己决定 |
ACC_ENUM | 0x4000 | 字段是否为枚举 |
00 03 00 01 00 0a 00 0b 00 00 00 01 00 0c 00 0b 00 00 00 01 00 0d 00 0e 00 00
位值 | 说明 |
---|---|
第一个成员字段 | |
00 03 | 代表字段数量有3个 |
00 01 | 代表字段访问标志位public |
00 0a | 代表名称索引为10 名称为 m1 |
00 0b | 代表descriptor的索引值为11 对象类型为Ljava/lang/String;【分号;全限定名】 |
00 00 | 代表没有属性数量 |
第二个成员字段 | |
00 01 | 代表字段访问标志位为public |
00 0c | 代表字段名称索引为12,名称为m2 |
00 0b | 代表类型的索引11,类型为 Ljava/lang/String;同上 |
00 00 | 代表没有属性 |
第三个成员字段 | |
00 01 | 同上 |
00 0d | 名称索引为13,经查找为arr |
00 0e | 类型索引为14,经查找为 [Ljava/lang/Object;全限定名,其中“[”代表为数组 |
00 00 | 代表没有属性,数量为0 |
唠嗑时间开始,写到这里花了三个多小时。从排版到书写上面确实有很大的提升。此时的我确实有点疲惫。仔细一想,没啥子疲惫不疲惫的,路虽远,但始终在路上,总会到达终点。突然想到书上说过这样的一句:当你在解决一个问题的时候,你会感到很疲惫,这时候千万别放弃。因为大部分的人就此放弃了,而你还在路上行走。当你解决之后,你又比别人强了不少!
写到这里【字段表集合】之后,这里就会很轻松。这里再啰嗦一下,字段表分为:成员字段数量、字段名索引、字段类型索引、字段属性数量、字段属性集合。
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
method_count | 方法数量 | ||
u2 | access_flag | 1 | 访问标志 |
u2 | name_index | 1 | 名称索引 |
u2 | descriptor_index | 1 | 类型索引 |
u2 | attributes_count | 1 | 属性数量 |
attributes_count | attributes_[attributes_count] | 1 | 属性表 |
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为公开 |
ACC_PRIVATE | 0x0002 | 方法是否为私有 |
ACC_PROTECTED | 0x0004 | 方法是否为保护 |
ACC_STATIC | 0x0008 | 方法是否为静态 |
ACC_FINAL | 0x0010 | 方法是否为Final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否在并发时可见 |
ACC_BRIDGE | 0x0040 | 方法是不是由编译器产生的桥接方法 |
ACC_VARCHAR | 0x0080 | 方法是否接收不可定参数 |
ACC_NATIVE | 0x0100 | 方法是否为Native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICT | 0x0800 | 方法是否为strictfp【修饰在接口和类,对精确率类型较高且跨平台的计算结果要求比较严格的清醒的话,建议使用该strictfp关键词。】 |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
继续上方法表的字节码:
00 02 00 01 00 0f 00 10 00 01 00 11 00 00 00 2f 00 00 00 01 00 00 00 05 2a b7 00 01 b1 00 00
现在对照上面的说明,现在逐一说明:
对照 | 位码 | 说明 |
---|---|---|
methods_count | 00 02 | 存在2个方法 |
acc_flags | 00 01 | 访问标识 public |
name_index | 00 0f | 方法名索引为15,经查为< init > |
descriptor_index | 00 10 | 方法描述索引为16,经常为()v |
attributes_count | 00 01 | 属性只有一个 |
attributes_name_index | 00 11 | 属性名索引为17,经查为code |
attributes_length | 00 00 00 2f | 属性值长度为47 |
max_stack | 00 00 | 最大栈深为0 |
max_locals | 00 01 | 需要分配1个变量槽,根据同时生存的最大局部变量数和类型计算 |
code_length | 00 00 00 05 | 字节码长度 |
code | 2a | 查看指令表为aload_0 |
code | b7 | 同上~,invokespecial,调用超类构造方法,实例初始化方法,私有方法 |
code | 00 | 同上~,nop,什么都不做 |
code | 01 | 同上~,acoust_null,将null推送至栈顶 |
code | b1 | 同上~,return ,从当前方法返回void |
exception_table_lenght | 00 00 | 当前没有发现异常信息 |
attributes_count | 00 02 | 该方法的附加属性共有2个 |
attribute_name_index | 0012 | 属性名索引为18,经查为 LineNumberTable |
attribute_length | 00 00 00 06 | 属性长度为6 |
line_number_table_length | 00 01 | 字节码行号共1行 |
star_pc | 00 00 | 从字节码第0行开始。此处说的行数是一种抽象的,指的是相对于方法体的偏移 |
line_number | 00 03java行号为3 | |
attribute_name_index | 00 13 | 属性名称索引为19,经查为 LocalVariableTable |
attribute_length | 00 00 00 0c | 属性长度为12 |
local_variable_table_length | 00 01 | 局部变量表长度为1 |
star_pc | 00 00 | 局部变量的生命周期开始的字节码偏移量 |
length | 00 05 | 往后偏移5个地址的长度,star_pc和length的配合使用就是局部变量在字节码中的作用域范围 |
name_index | 00 14 | 名字索引为20,经查为 this |
descriptor_index | 00 15 | 描述索引为21,经查为Lcom/company/jvm/Sample; |
index | 00 00 | 这个局部变量在栈帧的局部变量表中变量槽的为之为0 |
acc_flag | 00 09 | public的标志0x0001,static的标志0x0008,0x0001 |
name_index | 00 16 | 名字为索引22,经查为 main |
descriptor_index | 00 17 | 描述的索引为23,经查([Ljava/lang/String;)V |
attributes_count | 00 01 | 当前方法的属性长度为1 |
attribute_name_index | 00 11 | 当前属性名称索引值为17,经查为Code |
attribute_length | 00 00 00 66 | 当前属性的长度为102 |
attribute_name_index | 00 02 | 属性名称索引为2,经查为com/company/jvm/Sample |
max_stack | 00 02 | 栈帧最大深度为2 |
max_local | 00 02 | 最大局部变量槽数为2 |
code_lenght | 00 00 00 22 | 字节码长度为34 |
code | bb | 经查字节码指令,0xbb为new |
code | 00 | Nop,什么事都不做 |
code | 02 | 同上经查,为将int型-1推送至栈顶 |
code | 59 | 0x59,经查为dump,赋值栈顶数值并压入栈顶 |
code | b7 | 0xb7,invokespecial,调用超类构造方法,实例化初始方法,私有方法 |
code | 00 | Nop 不做任何事 |
code | 03 | 0x03,将int的0推送至栈顶 |
code | 0x27e | 一直都这个位都是code值 |
exception_table_lenght | 00 00 | 说明没有任何异常信息 |
attributes_count | 00 02 | 说明有两条 |
同上面分析~ | 同上面分析~ | 同上面分析~ |
到最后8位为Source
标识 | 位码 | 说明 |
---|---|---|
attribute_name_index | 00 1b | 名称索引为27,经查为SourceFile |
attribute_length | 00 00 00 02 | 对应值为2 |
sourcefile_index | 00 1c | 对应的文件索引值为28, 经查为Sample.java |
至此一个简单的文件就翻译完成了,对照着javap和字节码整理之后,确实有一番收获。但是还处于道可道非常道的过程,仍然需要透彻一些!明天继续干~