[讨论] 请问,jvm实现读取class文件常量池信息是怎样呢?

all_wmh 2011-06-19

在看class文件结构的时候,class文件中有一个常量池,我想问下 JVM实现 读取class文件常量池信息的时候,是不是根据class文件的常量池的tag信息分别读取到各个类型的常量表(比如CONSTANT_Utf8_info、CONSTANT_Class_info等等)中呢?

 

RednaxelaFX 2011-06-20
具体实现么…

================================================

HotSpot VM的话,当前的版本是这样的:

HotSpot VM用instanceKlass来记录类的元数据,每个Java类有一个对应的instanceKlass。
每个instanceKlass上引用着一个constantPoolOopDesc对象,然后间接引用着一个constantPoolCacheOopDesc对象。前者跟Class文件里记录的常量池的结构类似,而后者是为了让解释器运行得更高效的一个缓存。

举例的话,用VisualVM里的SA Plugin来演示,java.lang.String的状况。
这里我用JDK 7的一个预览版,build 96来运行VisualVM 1.3和一个groovysh,并且用VisualVM里的SA Plugin来观察groovysh的运行状态:

图1:java.lang.String对应的一个instanceKlass

留意到instanceKlass里有个_constants字段,引用着一个constantPoolOopDesc对象(后面简称constantPool对象)。

图2:观察constantPool对象的内容:

留意到它是一个类似数组的对象,里面有_length字段描述常量池内容的个数,后面就是常量池项了。
各个类型的常量是混在一起放在常量池里的,跟Class文件里的基本上一样。
最不同的是在这个运行时常量池里,symbol是在类之间共享的;而在Class文件的常量池里每个Class文件都有自己的一份symbol内容,没共享。

图3:观察constantPool里其中一个Utf8常量的内容:

这张图的关注点是位于0x180188a8的一个symbol对象(内容是"intern"),它的结构跟数组类似,有_length来记录长度,后面是UTF-8编码的字节。

这些Utf8常量在HotSpot VM里以symbolOopDesc对象(下面简称symbol对象)来表现;它们可以通过一个全局的SymbolTable对象找到。注意:constantPool对象并不“包含”这些symbol对象,而只是引用着它们而已;或者说,constantPool对象只存了对symbol对象的引用,而没有存它们的内容。

让我们来看看原本的Class文件里内容是怎样的:
D:\temp\jdk7b96\jdk1.7.0\fastdebug\bin>javap -verbose -private java.lang.String | more
Classfile jar:file:/D:/temp/jdk7b96/jdk1.7.0/fastdebug/jre/lib/rt.jar!/java/lang/String.class
  Last modified 2010-6-3; size 23741 bytes
  MD5 checksum 293ab9f6781f6cd7d8f1dcaeabf1701c
  Compiled from "String.java"
public final class java.lang.String extends java.lang.Object implements java.io.
Serializable, java.lang.Comparable<java.lang.String>, java.lang.CharSequence
  Signature: #405                         // Ljava/lang/Object;Ljava/io/Serializable;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/lang/CharSequence;
  SourceFile: "String.java"
  InnerClasses:
       static #134 of #40; //class java/lang/String$1 of class java/lang/String
       private static #137= #128 of #40; //CaseInsensitiveComparator=class java/lang/String$CaseInsensitiveComparator of class java/lang/String
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
    #1 = Methodref          #130.#408     //  java/lang/Object."<init>":()V
    #2 = Fieldref           #40.#409      //  java/lang/String.offset:I
    #3 = Fieldref           #40.#410      //  java/lang/String.count:I
    #4 = Fieldref           #40.#411      //  java/lang/String.value:[C
    #5 = Methodref          #412.#413     //  java/util/Arrays.copyOfRange:([CII)[C
    #6 = Methodref          #412.#414     //  java/util/Arrays.copyOf:([CI)[C
    #7 = Class              #415          //  java/lang/StringIndexOutOfBoundsException
    #8 = Methodref          #7.#416       //  java/lang/StringIndexOutOfBoundsException."<init>":(I)V
    #9 = Integer            65536
   #10 = Methodref          #417.#418     //  java/lang/Character.isSupplementaryCodePoint:(I)Z
   #11 = Class              #419          //  java/lang/IllegalArgumentException
   #12 = Methodref          #420.#421     //  java/lang/Integer.toString:(I)Ljava/lang/String;
   #13 = Methodref          #11.#422      //  java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V

...

再对比图2看看,是不是正好对应上的?

图2里constantPool的第一个常量池项的内容是:
JVM_CONSTANT_Methodref: 26738818

这个26738818数字是怎么来的呢?
实际上是:26738818 = 408 << 16 | 130
而原本Class文件里常量池的第一项内容正是#130.#408,也就是由一个Class_index和一个NameAndType_index组成的Methodref。

图2里还有个细节,可以看到原本Class文件里常量池第7项是一个Class,但在图2里显示的是一个“UnresolvedClass”。这正是动态类加载/链接的一个表现。这个项所指向的Class还没被String里的方法使用过,所以还没跟String链接起来,所以这里看到是unresolved。
我们可以故意在那个groovysh里执行一句:
'abc'.charAt(5)

这样会引发String.charAt()方法执行的过程中抛出一个java.lang.StringIndexOutOfBoundsException异常,那么就必须要完成链接的步骤。
然后再去看看String的常量池的样子:

就可以看到常量池的第7项已经解析(resolve)好了,从原本的符号引用变成了一个直接引用。

================================================

在JDK7以后的更新版中,HotSpot VM会逐渐去除PermGen,原本一些放在GC堆里的元数据会搬到GC管理之外的堆空间里。所以上面描述的实现会有些变化。具体会变成怎样还没真相。

================================================

至于其它JVM,其实运行时常量池想怎么组织都可以的,反正Java层面上看不出来JVM内部组织这些元数据的方式的差异。
xuhang1128 2011-06-29
这个visualvm是自带的吗,怎么功能好像强大很多啊
RednaxelaFX 2011-06-29
xuhang1128 写道
这个visualvm是自带的吗,怎么功能好像强大很多啊

虽然我用来截图的这个确实不是JDK自带的那个,但其实功能一样,主要是要多装些插件。上面的截图都是VisualVM的SA Plugin插件的
all_wmh 2011-06-30
引用
这个26738818数字是怎么来的呢?
实际上是:26738818 = 408 << 16 | 130

撒迦能否解释的更具体点呢? 还是没看明白。
RednaxelaFX 2011-06-30
all_wmh 写道
引用
这个26738818数字是怎么来的呢?
实际上是:26738818 = 408 << 16 | 130

撒迦能否解释的更具体点呢? 还是没看明白。

嗯,好啊。

先看JVM规范里Class文件格式中,CONSTANT_Field_info的结构:
JVMS2 写道
CONSTANT_Fieldref_info {
  u1 tag;
  u2 class_index;
  u2 name_and_type_index;
}

可以看到这个常量池项的结构就是一个字节的tag(= CONSTANT_Fieldref (9)),后面跟着两个字节的class_index,再跟着两个字节的name_and_type_index。

javap在显示常量池项的时候,这例子对应的项是:
#1 = Methodref          #130.#408     //  java/lang/Object."<init>":()V

实际上在Class文件里的样子是(十六进制):
09 00 82 01 98

也就是:
CONSTANT_Fieldref_info {
  u1 tag = 0x09;                   // CONSTANT_Fieldref
  u2 class_index = 0x0082;         // 130
  u2 name_and_type_index = 0x0198; // 408
}


HotSpot VM加载了这个Class文件后,在constantPool对象里也存下了对应的内容。用SA Plugin我们可以看到显示的是:
JVM_CONSTANT_Methodref: 26738818

实际上结构跟Class文件里原本的样子是一样的,也是有个tag来区分常量项的类型,后面指向别的常量项的索引号。
只不过SA Plugin在显示常量项内容的时候,把class_index与name_and_type_index的存放顺序跟Class文件里的正好反了过来组合成一个整数显示了出来而已。实际上26738818 = 408 << 16 | 130,也就是26738818作为一个4字节的整数,其中两字节是name_and_type_index,另外两字节是class_index,就这样。
all_wmh 2011-06-30
感谢撒迦的细心指导,非常感谢。
Global site tag (gtag.js) - Google Analytics