JAVA Class文件的常量池内容解析

之前学习多线程的时候接触到javap反汇编指令,其中javap -verbose指令能够把Class文件中的内容详细输出出来,其中不仅仅包括反汇编后的虚拟机指令,还包括了常量池中的内容。当时还不太明白反汇编后的常量池内容含义就放了一段时间,现在又遇到这个知识点就写一篇笔记备忘。

注:本篇文章中会使用到javap指令和VSCode的Hexdump插件。

常量池

常量池是Class文件中占用空间最大的也是Class文件中第一个出现的项目,由于常量池内的常量数量不确定,因此常量池内的第一个数据是一个u2类型的无符号数,其含义是常量池的容量。值得注意的是常量池的计数是从1开始的而不是编程中习惯的0开始,这是因为在设计之初0这个特殊值被考虑作为一种特殊情况而得以保留,此处不作展开。在常量池的容量计数器之后的项目就是常量池内的具体内容了。常量池内的每一个项目都是一个表,在JDK1.7之前总共有11种表对应不同的类型,在JDK1.7中扩充至14种。这14种表的结构都不相同,但有一个共同点,每个表的第一项都是一个u1类型的tag值,用于表明表的类型。

以下是tag值对应的表类型,取自《深入理解java虚拟机》

由于每一种表的结构都不相同,要理解每一项的内容还需要知道表的结构,以下为表结构汇总表,出处同上

OK,知识储备完成,现在开始具体分析。对应javap的反汇编结果查看以上两张表我们就能获悉常量池的具体内容了。此处我以最简单的Helloworld为例,具体代码以及部分反汇编结果如下

public class Hello{
	public static void main(String[] args){
		System.out.println("Hello world!");
	}
}
D:\Programs\JAVA>javap -verbose Hello.class
Classfile /D:/Programs/JAVA/Hello.class
  Last modified 2019-11-11; size 416 bytes
  MD5 checksum 2c1d43ed3833eb9bb18d2619f2cc53b2
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Hello world!
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // Hello
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Hello.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello world!
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               Hello
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public Hello();
    descriptor: ()V
    flags: 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

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello world!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
}
SourceFile: "Hello.java"

以上是反汇编的结果,但是要想真正了解Class文件的结构我们还需要查看Class源文件,这里我放上了Hello.class源文件的16进制代码,Class文件直接用文本方式打开是无法查看其字节码的,此处使用VSCode的Hexdump插件解析Class文件

前8个字节CA FE BA BE 00 00 00 34分别为Class文件的魔数(image number)CAFABABE和版本号52(34的十进制为52,对应Java 1.8版本),此处不做过多讨论。接下来第08、09偏移位置的值为001D,即为29,查看javap -verbose反汇编出来的代码我们可以发现,常量池有28项,以此我们知道08、09偏移位置的信息就是常量池的容积(上文提及0为特殊情况而保留不参与计数,因此从1开始计算常量池项目数总共有28项)。由于常量池容积为一个u2类型的无符号数,最大可表示范围为65535,如果一个文件中定义超过了这个数量,java文件将无法编译。

标红为常量池容积

常量池容积之后就是常量池内的第一个项目了,Class源文件中偏移位置为0A的列值为0A,对应十进制的10,查询tag值对应类型表我们知道这是一个方法的引用(Methodref_infor),再查询结构汇总表,发现方法引用这个类型的表含有3项,除第一项的tag以外,后两项都是u2类型的索引值,共占4个字节,索引的偏移量为06和0F,就是第6和第15项,分别为类描述和名称及类型描述。

标红为第2项内容

标红处为反汇编后代码的第1项内容,与Class文件中的内容一致

接着我们查看第2项,第2项的tag值为09,查表对应为字段引用(Fieldref_infor),根据结构汇总表,字段引用与第一项方法引用类似也含有3项,包括u1类型的tag和两个u2类型的index,分别为字段的类或接口描述符的索引和字段描述符的索引,两个索引的偏移量分别为10和11,也就是第16和17项。这样我们就知道了常量池中第二项为一个字段引用Fieldref,其中包含了对常量池中第16和17项的引用。

第三个常量的tag为08,查表后得知对应为一个字符串类型的字面量String_info,其包括了一个tag和一个u2类型的字面量的索引index,根据Class文件内容可以看到索引值为12,即对应常量池第18项。我们查看第18项,其tag为1,查表得知其为一个utf-8编码的字符串,内部包含了3各部分,u1类型的tag值,u2类型的字符串长度length,以及length个u1类型的utf-8编码字符。可以看到length值为0c,即字符串长度为12,lengh后的12个字节都是字符串的内容,即为48 65 6C 6C 6F 20 77 6F 72 6C 64 21,转换为字符就是“Hello world!”。也就是说“Hello world!”这个字符串是放在常量池第18项中的,被第三项引用。

第18项从第9行第01个偏移位置始到第0F个偏移位

此处就不再做过多的描述,文中分析了常量池中4项数据的具体内容,并对照Class源文件信息和javap -verbose反汇编的代码来帮助理解常量池内数据的具体存储方式。根据以上的方法,我们就能够获悉Class文件中常量池内容的具体结构和对应数据的含义了。Class文件中的重要内容不仅仅包含常量池,此处只对常量池作了分析,如有不正确的地方还请各位看官不吝赐教!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值