JDK6 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.CharSequence
Signature: #405 // Ljava/lang/Object;Ljava/io/Serializable;Ljava/lang/Comparable;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."":()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."":(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."":(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内部组织这些元数据的方式的差异。