目录
JVM要解析常量池里的数据,就要先进行内存分配,为常量池划分出一块内存空间,分配的空间需要多大?从哪里划出一片内存,这些在前面的日志里有总结过,在分配链路里有专门的函数根据常量池里的元素数量计算出需要的大小length,为常量池constantPoolOop对象分配的headSize(对象头)+length大小内存区域位于JVM的永久区permanent区,是一片连续的区域,最后进行清零等内存初始化操作。完成内存分配工作后,便可以往里面添加数据了,这就到了下一步,常量池解析工作。Java程序中的类,包括类中的变量和方法都会被JVM编译成字节码,在字节码文件里用常量池来描述这些类信息,当JVM加载某一个类时,从常量池中解析出类,还原里面定义的的变量和方法,所以需要构建constantPoolOop对象,为其分配足够的内存空间来保存字节码文件里的常量池信息,为后面加载类时解析操作做准备。
constantPoolOop初始化
初始化constantPoolOop过程调用的函数是constantPoolKlass::allocate(),该函数会对常量池对象进行内存分配,还会对里面的一些变量进行初始化:
constantPoolOop constantPoolKlass::allocate(int length, bool is_conc_safe,
TRAPS) {
//....
pool->set_length(length);
pool->set_tags(NULL);
pool->set_cache(NULL);
pool->set_operands(NULL);
pool->set_pool_holder(NULL);
pool->set_flags(0);
pool->set_orig_length(0);
pool->set_is_conc_safe(is_conc_safe);
//....
}
constantPoolOop实例对象内存布局前面日志里说过,分为对象头(_mark和_metadata),length数量个指针宽度的实际数据和常量池对象自己的字段,上面的allocate函数就是对这些变量进行初始化。其中_tag数组对于整个常量池对象来说十分重要,它存放了constantPoolOop中全部元素的标记,前面的日志通过十六进制字节码文件看常量池元素时也知道,每一个元素都是以tag位标开始的。初始化_tag时JVM也会为其分配length数量个指针宽度大小的内存,length前面说了是常量池中实际数据元素的个数,_tag元素也初始化完成后,此时里面为NULL没有存放任何数据,后面的常量池解析才会把数据填充进来。
解析
从JVM执行链路中,分配常量池内存oopFactory::new_constantPool()之后,到达下一条ClassFileParser::parse_constant_pool_entries()函数,由它来完成常量池信息的解析工作:
void ClassFileParser::parse_constant_pool_entries(constantPoolHandle cp, int
length, TRAPS) {
ClassFileStream* cfs0 = stream();
ClassFileStream cfs1 = *cfs0;
ClassFileStream* cfs = &cfs1;
const char* names[SymbolTable::symbol_alloc_batch_size];
int lengths[SymbolTable::symbol_alloc_batch_size];
int indices[SymbolTable::symbol_alloc_batch_size];
unsigned int hashValues[SymbolTable::symbol_alloc_batch_size];
int names_count = 0;
// parsing Index 0 is unused
for (int index = 1; index < length; index++) {
u1 tag = cfs->get_u1_fast();
switch (tag) {
case JVM_CONSTANT_Class :
{
cfs->guarantee_more(3, CHECK); // name_index, tag/access_flags
u2 name_index = cfs->get_u2_fast(); //获取类的名称索引
//将当前常量池元素的类型和名称索引分别保存到constantPoolOop的tag数组和数据区
cp->klass_index_at_put(index, name_index);
}
break;
case JVM_CONSTANT_Fieldref :
{
cfs->guarantee_more(5, CHECK); // class_index, name_and_type_index, tag/access_flags
u2 class_index = cfs->get_u2_fast();
u2 name_and_type_index = cfs->get_u2_fast();
cp->field_at_put(index, class_index, name_and_type_index);
}
break;
case JVM_CONSTANT_Methodref :
{
cfs->guarantee_more(5, CHECK); // class_index, name_and_type_index, tag/access_flags
u2 class_index = cfs->get_u2_fast();
u2 name_and_type_index = cfs->get_u2_fast();
cp->method_at_put(index, class_index, name_and_type_index);
}
break;
//....
}
解析工作具体从第一个for循环开始做起,每次循环第一步先做:
u1 tag = cfs->get_u1_fast();
表示从字节码文件中读取一个字节宽度的字节流,这一个字节长度的数据是每一个常量池元素开头的元素类型标识,获得元素类型后,通过switch条件表达式来对不同类型元素进行不同的处理,例如访问控制,获取类的名称索引,将索引保存到constantPoolOop的tag数组中。例如上图switch中的JVM_CONSTANT_Class表示的是常量池中该元素结构是宽度为u1的类型标识加上宽度为u2的名称索引,这里的u1u2前面说过是无符号数,分别标识1,2字节的长度,还有u4和u8。JVM_CONSTANT_Fielder表示元素结构是宽度为u1的类型标识加上宽度为u2的名称索引和宽度为u2的类索引。
类元素解析
获取了元素的名称索引后,接着通过cp->klass_index_at_put(index, name_index)将其保存到constantPoolOop里的tag数组中,举个例子:
一号常量池元素#1是JVM_CONSTANT_Class类型,它在tag数组中的位置对应也是1,在constantPoolOop中#1元素存储的值是2,也就是它的名称索引是2,tag的#1位置存储的值就是7,因为对应下图中的常量池元素枚举,类型JVM_CONSTANT_Class的枚举值为7。
enum {
JVM_CONSTANT_Utf8 = 1,
JVM_CONSTANT_Unicode, // unused
JVM_CONSTANT_Integer,
JVM_CONSTANT_Float,
JVM_CONSTANT_Long,
JVM_CONSTANT_Double,
JVM_CONSTANT_Class,
JVM_CONSTANT_String,
JVM_CONSTANT_Fieldref,
JVM_CONSTANT_Methodref,
JVM_CONSTANT_InterfaceMethodref,
JVM_CONSTANT_NameAndType,
JVM_CONSTANT_MethodHandle = 15,
JVM_CONSTANT_MethodType = 16,
//JVM_CONSTANT_(unused) = 17,
JVM_CONSTANT_InvokeDynamic = 18,
JVM_CONSTANT_ExternalMax = 18
}
方法元素解析
方法元素结构为u1宽度的元素类型标识加上一个u2宽度的类索引和一个u2宽度的方法名索引,索引保存在了constantPoolOop中,JVM为了在数据区一个位置上保存两个索引的值,采用了拼接的方法:
void method_at_put(int which, int class_index, int name_and_type_index) {
tag_at_put(which, JVM_CONSTANT_Methodref);
*int_at_addr(which) = ((jint) name_and_type_index<<16) | class_index);
}
具体的方式是通过左移两个字节,因为两个索引都是宽度为u2,即占两个字节的数据,从上面的javap指令分析字节码文件也可以看到,第#10号元素Methodref,其值为#3.#11,表示的是类索引是3,方法名索引是11,下面还有很多元素都采用到了拼接的方式存储。
字符串元素解析
最后到字符则元素的解析,在JVM里不论是类名、方法名还是指针名,都会被当作字符串来处理,存放到专门的符号表里,为什么要专门设置一个符号表来存放字符串呢?因为上面也看到,constantPoolOop中一个存储位置只能存放一个指针宽度的数据,容易出现一些字符串很大而无法存储,所以将它们放到符号表中,在常量池数据区里存放指针引用,指向符号表里的字符串,也就是说字节码文件里的常量池元素都会被保存到符号表中,再通过指针来引用。在存储时JVM会先去判断符号表里面有没有存字相同的字符串,如果有,则不需要申请内存来存储,而是直接让指针指向该字符串,例如你有一个类里面定义了两个字符串,它们的值都相同,那么这两个字符串变量都会指向同一个常量池位置。