java 类 自指属性_第五章 常量池解析

常量池解析

Java字节码常量池的内存分配链路

oop-klass模型

常量池的解析原理

在字节码文件中,常量池的字节码流所在的块区紧跟在魔数和版本号之后,因此JVM在解析完魔数与版本号后就开始解析常量池。JVM解析Java类字节码文件的接口:ClassFileParser::parseClassFile(),总体步骤如下:解析魔数–>解析版本号–>解析常量池–>解析父类–>解析接口–>解析类变量–>解析类方法–>构建类结构。JVM解析常量池信息主要链路:ClassFileParser::parseClassFile()–>ClassFileParser::parse_constant_pool()–>oopFactory::new_constantPool()(分配常量池内存);ClassFileParser::parse_constant_pool_entries()(解析常量池信息)。

常量池内存分配

常量池内存分配总体链路

ClassFileParser::parse_constant_pool()函数中,通过以下代码实现常量池内存分配:

onstantPoolOop constant_pool = oopFactory::new——constantPool(length,oopDesc::IsSafeConc,CHECK_(nullHandle));

length:代表当前字节码文件的常量池中一共包含多少个常量池元素,由Java编译器在编译期间通过计算得出,保存在字节码文件中。

oopFactory::new_constantPool()的链路比较长,下图展示了总体调用路径:

4ce23bf392da66b2f67913554810525a.png

oopFactory:oop的工厂类,JVM内部,查了那个量尺、字段、符号、方法等一切都被对象包装起来,在内存中通过oop指针进行跟踪,而这些对象的内存分配、对象创建与初始化工作都通过oopFactory这个入口实现。

constantPoolKlass:常量池类型对象。JVM内部预留的一段描述常量池结构信息的内存。

collectedHeap:JVM内部的堆内存区域,可被垃圾收集器回收并反复利用。其代表JVM内部广义的堆内存区域。除了堆栈变量之外的一切内存分配都需要经过本区域。

psPermGen:表示perm区内存,JDK1.6版本产物,Java类的字节码信息会保存到这块区域,JDK1.8后,perm去概念被metaSpace概念取代。

perm指内存的永久保存区域,这一部分用于存放Class和Meta的信息,Class在被加载的时候放入permGen space区域,如果加载了太多类就很可能出现PermGen space错误。而在metaSpace时代,这块区域是属于“本地内存”区域,也就是操作系统,相对于JVM虚拟机内存而言的,JVM直接向操作系统申请内存存储Java类的袁茜茜,如此一来可以解决为permGen space指定的内存太小而导致perm区内存耗尽的问题。

从宏观层面上看,常量池内存分配大体上可以分为以下三步:

在堆区分配内存空间

初始化对象

初始化oop

内存分配

从oopFactory::new_constantPool()调用开始,到mutableSpace:allocate()的过程可认为是第一阶段,即为constantPool申请内存。

JVM为constantPool申请内存链路

oopFactory.cpp(ck->allocate(length,is_conc_safe,CHECK_NULL))

constantPoolKlass.cpp(CollectedHeap::permanent_obj_allocate(klass, size, CHECK_NULL))

collectedHeap.inline.hpp(permanent_obj_allocate_no_klass_install(klass,size,CHECK_NULL))

collectedHeap.inline.hpp(common_permanent_men_allocate_init(size,CHECK_NULL))

collectedHeap.inline.hpp(common_permanent_men_allocate_noinit(size,CHECK_NULL))

collectedHeap.inline.hpp(Universe::heap()->permanent_men_allocate(size))

parallelScavengeHeap.cpp(perm_gen()->allocate_permanent(size))

psPermGen.cpp(allocate_noexpand(size,false))

paOldGen.hpp(objec_space()->allocate(word_size))

内存申请最终通过objec_space()->allocate(word_size)实现,定义如下:

HeapWord* MutableSpace::allocate(size_t size) {

assert(Heap_lock->owned_by_self() ||

(SafepointSynchronize::is_at_safepoint() &&

Thread::current()->is_VM_thread()),

"not locked");

HeapWord* obj = top();

if (pointer_delta(end(), obj) >= size) {

HeapWord* new_top = obj + size;

set_top(new_top);

assert(is_object_aligned((intptr_t)obj) && is_object_aligned((intptr_t)new_top),

"checking alignment");

return obj;

} else {

return NULL;

}

}

通过HeapWord* new_top = obj + size;将permSpace内存区域的top指针往高地址方向移动了size大小的字节数,完成了内存分配(仅仅从JVM申请的堆内存中划拨了一块空间用于存储常量池结构信息)

先执行HeapWord* obj = top();然后执行HeapWord* new_top = obj + size;返回的仍是原堆顶obj指针,可以通过指针还原从原堆顶到当前堆顶之间的内存空间,将其强转型为常量池对象。

JVM在启动过程中完成permSpace的内存申请和初始化,每次新写入一个对象,该对象的内存首地址便是写入之前top指针所指位置。

constantPool的内存有多大

为一个Java类创建其对应的常量池,需要在JVM堆区为常量池先申请一块连续空间,所申请的空间大小取决于一个Java类在编译时所确定的常量池大小(size),在常量池初始化链路中调用constantPoolKlass::allocate()方法,这个方法会调用constantPoolOopDesc::object_size(length)方法获取常量池大小,方法原型:

static int header_size() { return sizeof(constantPoolOopDesc)/HeapWordSize; }

static int object_size(int length) { return align_object_size(header_size() + length); }

object_size()的逻辑十分简单,就是将header_size和length相加后进行内存对齐。header_size()返回的是constantPoolOopDesc类型的大小。align_object_size()是实现内存对齐,便于GC进行工作时更加高效。在32位操作系统上,HeapWordSize大小为4,是一个指针变量的长度,在32位平台上sizeof(constantPoolOopDesc)返回40.

关于sizeof

当计算C++类时,返回的是其所有变量的大小加上虚函数指针大小,若在类中定义了普通函数,都不会计算其大小。

#includeclass A {

private:

char* a;

public:

int getA() {

return 1;

}

};

int main() {

printf("sizeof(A)=%d\n", sizeof(A));

return 0;

}

这一段程序在32位系统输出是4,64位系统输出为8,因为他只包含一个char型指针变量。如此一来我们可以推算sizeof(constantPoolOopDesc)的返回值大小

constantPoolOopDesc本身包含8个字段

typeArrayOop _tags

constantPoolCacheOop _cache

klassOop _pool_holder

typeArrayOop _operands

int _flags

int _length

volatile bool _is_conc_safe

int _orig_length

而由于constantPoolOopDesc继承自oopDesc类,因此还会包含父类的成员变量:

volatile markOop _mark; //指针

union _metadata{ //指针

wideKlassOop _klass;

narrowOop _compressed_klass;

}_metadata;

所以,constantPoolOopDesc最终包含10个字段,在32位平台上占40字节。

我们接下来在看HeapWordSize的定义:

const int HeapWordSize = sizeof(HeapWord);

class HeapWord {

friend class VMStructs;

private:

char* i;

#ifndef PRODUCT

public:

char* value() { return i; }

#endif

};

可以看到,HeapWordSize就是一个char*类型的大小,在32位平台是4,64位平台是8,所以return sizeof(constantPoolOopDesc)/HeapWordSize;返回的就是constantPoolOopDesc类型本身所占内存的总字节数。所以最终constantPoolKlass::allocate()从JVM堆中所申请的内存空间大小包含:constantPoolOopDesc大小和Java类常量池元素数量。

我们来举一个例子:

public class Iphone6S {

int length = 138;

int width = 67;

int height = 7;

int weight = 142;

int ram = 2;

int rom = 16;

int pixel = 1200;

}

字节码中显示常量池大小为0x0023,对应十进制35,因此常量池一共包含38个元素,再看javap反编译的信息:

Constant pool:

#1 = Methodref #10.#25 // java/lang/Object."":()V

#2 = Fieldref #9.#26 // JVM/Iphone6S.length:I

#3 = Fieldref #9.#27 // JVM/Iphone6S.width:I

#4 = Fieldref #9.#28 // JVM/Iphone6S.height:I

#5 = Fieldref #9.#29 // JVM/Iphone6S.weight:I

#6 = Fieldref #9.#30 // JVM/Iphone6S.ram:I

#7 = Fieldref #9.#31 // JVM/Iphone6S.rom:I

#8 = Fieldref #9.#32 // JVM/Iphone6S.pixel:I

#9 = Class #33 // JVM/Iphone6S

#10 = Class #34 // java/lang/Object

#11 = Utf8 length

#12 = Utf8 I

#13 = Utf8 width

#14 = Utf8 height

#15 = Utf8 weight

#16 = Utf8 ram

#17 = Utf8 rom

#18 = Utf8 pixel

#19 = Utf8 #20 = Utf8 ()V

#21 = Utf8 Code

#22 = Utf8 LineNumberTable

#23 = Utf8 SourceFile

#24 = Utf8 Iphone6S.java

#25 = NameAndType #19:#20 // "":()V

#26 = NameAndType #11:#12 // length:I

#27 = NameAndType #13:#12 // width:I

#28 = NameAndType #14:#12 // height:I

#29 = NameAndType #15:#12 // weight:I

#30 = NameAndType #16:#12 // ram:I

#31 = NameAndType #17:#12 // rom:I

#32 = NameAndType #18:#12 // pixel:I

#33 = Utf8 JVM/Iphone6S

#34 = Utf8 java/lang/Object

JVM会保留0号常量池位置,所以只有34个元素。按照上面分析,JVM会从permSpace中划分(40+35)*4字节的内存大小(32位平台)。

内存空间布局

JVM为常量池对象申请的内存位于perm区,perm区本事是一片连续的内存区域,而JVM为常量池申请内存时也是正片区域连续划分,因此每一个constantPoolOop对象实例在perm区中都是连续分布的,不会存在碎片化。

最终申请好的空间布局如图ConstantPoolOop内存布局.png所示(假设运行于Linux32位平台,指针宽度为4字节,并且常量池分配方向从低地址到高地址。JVM内部为对象分配内存时,先分配对象头,然后分配对象的实例数据,不管字段对象还是方法对象亦或是数组,都是如此。

初始化内存

JVM为Java类所对应的常量分配好内存空间后,接着需要对这段内存空间进行初始化(实际上是清零操作)。在Linux32位平台上最终调用pd_fill_to_words()函数,声明如下:

static void pd_fill_to_words(HeapWord* tohw, size_t count, juint value) {

#ifdef AMD64

julong* to = (julong*) tohw;

julong v = ((julong) value << 32) | value;

while (count-- > 0) {

*to++ = v;

}

#else

juint* to = (juint*)tohw;

count *= HeapWordSize / BytesPerInt;

while (count-- > 0) {

*to++ = value;

}

#endif // AMD64

}

可以看到这个函数有3个入参,会将指定内存区的内存数据全部清空为value值,在CollectedHeap::init_obj()中调用了Copy::fill_to_aligned_words(obj+hs,size-hs)函数,而Copy::fill_to_aligned_words()函数有三个入参,声明如下:

static void fill_to_aligned_words(HeapWord* to, size_t count, juint value = 0) {

assert_params_aligned(to);

pd_fill_to_aligned_words(to, count, value);

}

该函数第三个参数默认为0,所以在执行pd_fill_to_aligned_words()函数时,指定的内存区会全部清零。

oop-klass模型

两模型三维度

JVM内部基于oop-klass模型描述一个Java类,将一个Java类一拆为二,分别描述,第一个模型是oop,第二个模型是klass。所谓oop,实际上是指ordinary object pointer(普通对象指针),用来表示对象的实例信息,看起来像个指针,实际上对象实例数据都藏在指针所指向的内存首地址后面的一片内存区域中。而klass则包含元数据和方法信息,用来描述Java类或者JVM内部自带的C++类型信息,Java类的继承信息、成员变量、静态变量、成员方法、构造函数等信息都在klass中保存。JVM由此便可以在运行期反射出Java类的全部结构信息。

oop模型:侧重于描述Java类的实例数据,为Java类生成一张“实例数据视图”,从数据维度描述一个Java类实例对象中各属性在运行期的值

klass模型

Java类的“元信息视图”,为JVM在运行期呈现Java类的全息数据结构信息,是JVM在运行期的移动台反射出类信息的基础。

虚函数列表(方法分发规则)

tip:Java类的所有函数都视为是“virtual”的,这样Java类的每个方法都可以直接被其子类覆盖而不需要添加任何关键字作为修饰符,因此,Java类中的每个方法都可以晚绑定,而也正是因为所有函数都视为虚函数,所以在JVM内部的C++就必须维护一套函数分发表。

体系总览

82c1fbe63d4ad6acbb3748ba38d083eb.png

handle是对oop的行为的封装,在访问Java类时,一定是通过handle内部指针得到oop示例的,在通过oop就能拿到klass,如此handle最终便能操纵oop的行为了。

Handle类的基本结构:

class Handle VALUE_OBJ_CLASS_SPEC {

private:

oop* _handle;

//省略部分代码

};

可以看到Handle内部只有一个成员变量_handle指向oop类型,因此该变量指向的是一个oop首地址。oop一般由对象头、对象专有属性和数据体三部分构成,结构如图

oop模型.png

JVM内部定义了若干oop类型,每一种oop类型都有自己特有的数据结构,oop的专有属性区便是用于存放各个oop所特有的数据结构的地方。

oop体系

究竟什么是普通对象指针?

Hotspot里的oop指什么

Hotspot里的oop其实就是GC所托管的指针,所有oopDesc及其子类(除了markOopDesc外)的实例都是由GC所管理

对象指针前为何冠以“普通”二字

在HotSpot里面,oop就是指一个真正的指针,而markOop则是一个看起来像指针,实际上是藏在指针里面的对象(数据),并没有指向内存的功能,这也正是它不受GC托管的原因,只要除了函数作用域,指针变量就会直接从堆栈上释放,不需要垃圾回收了。

oop的继承体系:

typedef class oopDesc* oop; //所有oop顶级父类

typedef class instanceOopDesc* instanceOop; //表示Java类实例

typedef class methodOopDesc* methodOop; //表示Java方法

typedef class constMethodOopDesc* constMethodOop; //表示Java方法中的只读信息(字节码指令)

typedef class methodDataOopDesc* methodDataOop; //表示性能统计的相关数据

typedef class arrayOopDesc* arrayOop; //表示数组对象

typedef class objArrayOopDesc* objArrayOop; //表示引用类型数组对象

typedef class typeArrayOopDesc* typeArrayOop; //表示基本类型数组对象

typedef class constantPoolOopDesc* constantPoolOop; //表示Java字节码文件中的常量池

typedef class constantPoolCacheOopDesc* constantPoolCacheOop; //与constantPoolOop伴生,是后者的缓存对象

typedef class klassOopDesc* klassOop; //指向JVM内部的klass实例的对象

typedef class markOopDesc* markOop; //oop的标记对象

typedef class compiledICHolderOopDesc* compiledICHolderOop;

klass体系

klass提供一个与Java类对等的C++类型描述

klass提供虚拟机内部的函数分发机制

klass的继承体系:

class Klass; //klass家族基类

class instanceKlass; //虚拟机层面上与Java类对等的数据结构

class instanceMirrorKlass; //描述java.lang.Class的实例

class instanceRefKlass; //描述java.lang.ref.Reference的子类

class methodKlass; //表示Java类的方法

class constMethodKlass; //描述Java类方法所对应的字节码指令信息的固有属性

class methodDataKlass;

class klassKlass; //klass链路末端,在jdk8中已经不存在

class instanceKlassKlass;

class arrayKlassKlass;

class objArrayKlassKlass;

class typeArrayKlassKlass;

class arrayKlass; //描述Java数组的信息,是抽象基类

class objArrayKlass; //描述Java引用类型数组的数据结构

class typeArrayKlass; //描述Java中基本类型数组的数据结构

class constantPoolKlass; //描述Java字节码文件中的常量池的数据结构

class constantPoolCacheKlass;

class compiledICHolderKlass;

基类klass定义:

class Klass : public Klass_vtbl {

friend class VMStructs;

protected:

enum { _primary_super_limit = 8 };

jint _layout_helper;

juint _super_check_offset;

Symbol* _name;

public:

oop* oop_block_beg() const { return adr_secondary_super_cache(); }

oop* oop_block_end() const { return adr_next_sibling() + 1; }

protected:

klassOop _secondary_super_cache;

objArrayOop _secondary_supers;

klassOop _primary_supers[_primary_super_limit];

oop _java_mirror;

klassOop _super;

klassOop _subklass;

klassOop _next_sibling;

//

// End of the oop block.

//

jint _modifier_flags; // Processed access flags, for use by Class.getModifiers.

AccessFlags _access_flags; // Access flags. The class/interface distinction is stored here.

juint _alloc_count; // allocation profiling support - update klass_size_in_bytes() if moved/deleted

// Biased locking implementation and statistics

// (the 64-bit chunk goes first, to avoid some fragmentation)

jlong _last_biased_lock_bulk_revocation_time;

markOop _prototype_header; // Used when biased locking is both enabled and disabled for this type

jint _biased_lock_revocation_count;

//省略部分代码

};

字段名

含义

_layout_helper

对象布局的综合描述符

_name

类名,如java/lang/String

_java_mirror

类的镜像类

_super

父类

_subklass

指向第一个子类

_next_sibling

指向下一个兄弟结点

_modifier_flags

修饰符标识,如static

_access_flags

访问权限标识,如public

如果一个KLASS既不是instance也不是array,则_layout_helper被设置为0,若是instance,则为正数,若是数组,则为负数。

handle体系

handle通过oop拿到klass,可以说是对普通对象的一种间接引用,这完全是为GC考虑

通过handle能够让GC知道其内部代码有哪些地方持有GC所管理的对象的引用,只需要扫码handle对应的table,JVM无需关注其内部到底哪些地方持有对普通对象的引用

在GC过程中,如果发生了对象移动(如从新生代移到老年代)那么JVM内部引用无需跟着更改为被移动对象的新地址,只需要修改handle table的对应指针。

在C++领域,类的继承和多态性最终通过vptr(虚函数表)来实现,在klass内部记录了每一个类的vptr信息

vptr虚函数表

存放Java类中非静态和非private方法入口,jVM调用Java类的方法(非static和非private)时,最终访问vtable找到对应入口

itable接口函数表

存放java类所实现的接口类方法,JVM调用接口方法时,最终访问itable找到对应入口。

vtable和itable并不存放Java类方法和接口方法的直接入口,而是指向了Method对象入口。

handle体系家族:

// Specific Handles for different oop types oop家族对应的handle宏定义

#define DEF_HANDLE(type, is_a) \

class type##Handle; \

class type##Handle: public Handle { \

protected: \

type##Oop obj() const { return (type##Oop)Handle::obj(); } \

type##Oop non_null_obj() const { return (type##Oop)Handle::non_null_obj(); } \

\

public: \

/* Constructors */ \

type##Handle () : Handle() {} \

type##Handle (type##Oop obj) : Handle((oop)obj) { \

assert(SharedSkipVerify || is_null() || ((oop)obj)->is_a(), \

"illegal type"); \

} \

type##Handle (Thread* thread, type##Oop obj) : Handle(thread, (oop)obj) { \

assert(SharedSkipVerify || is_null() || ((oop)obj)->is_a(), "illegal type"); \

} \

\

/* Special constructor, use sparingly */ \

type##Handle (type##Oop *handle, bool dummy) : Handle((oop*)handle, dummy) {} \

\

/* Operators for ease of use */ \

type##Oop operator () () const { return obj(); } \

type##Oop operator -> () const { return non_null_obj(); } \

};

DEF_HANDLE(instance , is_instance )

DEF_HANDLE(method , is_method )

DEF_HANDLE(constMethod , is_constMethod )

DEF_HANDLE(methodData , is_methodData )

DEF_HANDLE(array , is_array )

DEF_HANDLE(constantPool , is_constantPool )

DEF_HANDLE(constantPoolCache, is_constantPoolCache)

DEF_HANDLE(objArray , is_objArray )

DEF_HANDLE(typeArray , is_typeArray )

// Specific KlassHandles for different Klass types klass家族对应的handle宏定义

#define DEF_KLASS_HANDLE(type, is_a) \

class type##Handle : public KlassHandle { \

public: \

/* Constructors */ \

type##Handle () : KlassHandle() {} \

type##Handle (klassOop obj) : KlassHandle(obj) { \

assert(SharedSkipVerify || is_null() || obj->klass_part()->is_a(), \

"illegal type"); \

} \

type##Handle (Thread* thread, klassOop obj) : KlassHandle(thread, obj) { \

assert(SharedSkipVerify || is_null() || obj->klass_part()->is_a(), \

"illegal type"); \

} \

\

/* Access to klass part */ \

type* operator -> () const { return (type*)obj()->klass_part(); } \

\

static type##Handle cast(KlassHandle h) { return type##Handle(h()); } \

\

};

DEF_KLASS_HANDLE(instanceKlass , oop_is_instance_slow )

DEF_KLASS_HANDLE(methodKlass , oop_is_method )

DEF_KLASS_HANDLE(constMethodKlass , oop_is_constMethod )

DEF_KLASS_HANDLE(klassKlass , oop_is_klass )

DEF_KLASS_HANDLE(arrayKlassKlass , oop_is_arrayKlass )

DEF_KLASS_HANDLE(objArrayKlassKlass , oop_is_objArrayKlass )

DEF_KLASS_HANDLE(typeArrayKlassKlass , oop_is_typeArrayKlass)

DEF_KLASS_HANDLE(arrayKlass , oop_is_array )

DEF_KLASS_HANDLE(typeArrayKlass , oop_is_typeArray_slow)

DEF_KLASS_HANDLE(objArrayKlass , oop_is_objArray_slow )

DEF_KLASS_HANDLE(constantPoolKlass , oop_is_constantPool )

DEF_KLASS_HANDLE(constantPoolCacheKlass, oop_is_constantPool )

JVM内部,为了让GC便于回收(既能回收一个类实例所对应的实例数据oop,也能回收其对应的元数据和需方法表klass[一个是堆区的垃圾回收,一个是永久区的垃圾回收]),将oop本身鞥装成了oop,而klass也被封装成了oop。

oop、klass、handle的转换

1 从oop和klass到handle

handle主要用于封装组昂oop和klass,因此往往在声明handle类实例的时候,直接将oop或者klass传递进去完成封装。在JVM执行Java类方法时,最终也是通过handle拿到对应的oop和klass,为了高效快速调用,JVM重载了类方法访问操作符->。

Handle的构造函数:

inline Handle::Handle(oop obj) {

if (obj == NULL) {

_handle = NULL;

} else {

_handle = Thread::current()->handle_area()->allocate_handle(obj);

}

}

这个构造函数接收oop类型参数,并将其保存到当前线程在堆区申请的handleArea表中,由于oop仅是一种指针,所以表中存储的实际上也是指针。

默认构造函数:

// Constructors

Handle() { _handle = NULL; }

Handle重载操作符->:

oop non_null_obj() const { assert(_handle != NULL, "resolving NULL handle"); return *_handle; }

oop operator -> () const { return non_null_obj(); }

治理定义了oop operate ->()操作符重载函数,返回non_null_obj(),而后者直接返回oop类型的*_handle类型,因此,如果JVM想要调用oop的某个函数,可以直接通过handle。

klass体系的Handle类全部继承与KlassHandle这个基类,与oop体系类似,KlassHandle中也定义了多种构造函数用来实现对klass或oop的封装,并且重载了operate ->()函数实现从handle直接调用klass的函数。JVM创建Java类所对应的类模型时便使用了这种方式。

instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,

Handle class_loader,

Handle protection_domain,

KlassHandle host_klass,

GrowableArray* cp_patches,

TempNewSymbol& parsed_name,

bool verify,

TRAPS){

// We can now create the basic klassOop for this klass

klassOop ik = oopFactory::new_instanceKlass(name, vtable_size, itable_size,

static_field_size,

total_oop_map_count,

rt, CHECK_(nullHandle));

instanceKlassHandle this_klass (THREAD, ik);

// Fill in information already parsed

this_klass->set_access_flags(access_flags);

this_klass->set_should_verify_class(verify);

jint lh = Klass::instance_layout_helper(instance_size, false);

this_klass->set_layout_helper(lh);

assert(this_klass->oop_is_instance(), "layout is correct");

assert(this_klass->size_helper() == instance_size, "correct size_helper");

// Not yet: supers are done below to support the new subtype-checking fields

//this_klass->set_super(super_klass());

this_klass->set_class_loader(class_loader());

this_klass->set_nonstatic_field_size(nonstatic_field_size);

this_klass->set_has_nonstatic_fields(has_nonstatic_fields);

}

在该函数中,先通过klassOop ik = oopFactory::new_instanceKlass();创建了一个klassOopDesc实例,接着通过instanceKlassHandle this_klass (THREAD, ik); 将oop封装到了instanceKlassHandle中,接下来便通过this_klass指针来直接调用instanceKlass中的各种函数。

2 klass和oop的相互转换

为了便于GC回收,每一种klass实例最终都要被封装成对应的oop,具体操作是先分配对应的oop实例,然后将klass实例分配到oop对象头后面,从而实现oop+klass这种内存布局结构。对于任何一种给定的oop和其对应的klass,oop对象首地址到其对应的klass对象首地址的距离是固定的,因此只要得到一个便能根据相对寻址找出另一个。对于每一种oop,都提供了klass_part()函数,可以直接由oop得到对应的klass实例。

klass_part()原型:

Klass* klass_part() const { return (Klass*)((address)this + sizeof(klassOopDesc)); }

同样,将klass转换为oop只需要对klass首地址做减法

假设在某个Java方法中连续实例化了三个Student类,那么JVM内存中会出现如下图的布局:

常量池klass模型

到目前为止,JVM为constantPoolOop所分配的内存区域还是空的,但是,在ClassFileParser:parse_constant_pool()函数中执行完oopFactory::new_constantPool()函数时,已经为constantPoolOop初始化好了_metadata所指向的实例。

constantPoolOop oopFactory::new_constantPool(int length,

bool is_conc_safe,

TRAPS) {

constantPoolKlass* ck = constantPoolKlass::cast(Universe::constantPoolKlassObj());

return ck->allocate(length, is_conc_safe, CHECK_NULL);

}

constantPoolKlass* ck = constantPoolKlass::cast(Universe::constantPoolKlassObj());这行代码调用全局对象Universe的静态函数constantPoolKlassObj()来获取constantPoolKlass实例指针,逻辑如下:

static klassOop constantPoolKlassObj(){

return _constatntPoolKlassObj;

}

这个函数直接返回了全局静态变量_constantPoolKlassObj,该变量在JVM启动过程中被实例化,JVM实例化过程会调用如下逻辑:

klassOop constantPoolKlass::create_klass(TRAPS) {

constantPoolKlass o;

KlassHandle h_this_klass(THREAD, Universe::klassKlassObj());

KlassHandle k = base_create_klass(h_this_klass, header_size(), o.vtbl_value(), CHECK_NULL);

// Make sure size calculation is right

assert(k()->size() == align_object_size(header_size()), "wrong size for object");

java_lang_Class::create_mirror(k, CHECK_NULL); // Allocate mirror

return k();

}

这个函数获取了klassKlass实例,JVM初始化过程中会执行一下逻辑:

klassOop klassKlass::create_klass(TRAPS) {

KlassHandle h_this_klass;

klassKlass o;

// for bootstrapping, handles may not be available yet.

klassOop k = base_create_klass_oop(h_this_klass, header_size(), o.vtbl_value(), CHECK_NULL);

k->set_klass(k); // point to thyself

// Do not try to allocate mirror, java.lang.Class not loaded at this point.

// See Universe::fixup_mirrors()

return k;

}

这个逻辑中最终调用了base_create_klass_oop()函数创建klassKlass实例,该实例是全局性的,可以通过Universe::klassKlassObj()获取

至此,JVM启动过程中,先创建了klassKlass实例,在根据该实例创建了常量池对应的Klass类——constantPoolKlass。所以我们先要分析klassKlass实例的创建。

klassKlass实例的构建总链路

a4f4df567101725f8dc454ac08590c08.png

这部分总体分为六个步骤:

为klassOop申请内存

klassOop内存清零

初始化klassOop_metadata

初始化klass

自指

1.为klassOop申请内存

从进入klassKlass.cpp::create_klass()之后就一路开始调用层层封装的函数,直到CollectedHeap::permanent_obj_allocate_no_klass_install()才结束,这里开始在永久区为obj分配对象内存,这个函数的逻辑如下:

HeapWord* CollectedHeap::common_mem_allocate_init(size_t size, TRAPS) {

HeapWord* obj = common_mem_allocate_noinit(size, CHECK_NULL);

init_obj(obj, size);

return obj;

}

当这个函数执行完毕后,JVM的永久区便多了一个对象内存布局,该对象是klassKlassOop,由oop对象头(_mark和_metadata)和klassKlass实例区组成,而现在_metadata指向哪里呢?

2.klassOop内存清零

为klassOop分配完内存后,接下来便是将这段内存清零,之所以要清零,是因为JVM永久区是一个可以重复使用的内存区域,会被GC反复回收,因此刚刚申请的内存区域可能还保存着原来已经被清理的对象的数据。

3.初始化mark

经过上面两个步骤,接下来就要向内存填充数据了,oop一共包含两个成员变量:_mark和_metadata,这里先填充mark变量,主要通过调用post_allocate_setup_no_klass_install函数实现,主要调用了obj->set_mark(markOopDesc::prototype())函数。声明如下:

static markOop prototype() {

return markOop( no_hash_in_place | no_lock_in_place );

}

在C语言中,有一种快速赋初值的写法,例如

int x = 3;

//可以写成

int x(3);

//同样对于指针数据:

int x = 3;

int *p = &x;

//可以写成:

int x = 3;

int *p(&x);

markOop的实际类型是markOopDesc*类型,同样支持上面的写法,所以这个函数最终将返回一个指针,存储了一个整型数字,由于oop._mark成员变量用于存储JVM内部对象的哈希值,线程锁等信息,而此时返回的mark表示则标记当前oop尚未建立唯一哈希值。JVM内部每次读取oop的mark标识时会调用markOopDesc的value函数,定义如下:

uintptr_t value() const { return (uintptr_t) this; }

可以看到,内部其实是将this指针当做值使用,并没有指向markOopDesc实例,它仅仅用于存储JVM内部对象的哈希值、锁状态标识等信息。

4 初始化_metadata

初始化玩mark标记后,开始初始化_metadata成员,这是一个联合体,最终是NULL

5.初始化klass

现在已经完成了oop对象头初始化,接着开始初始化klassKlass类型实例Klass::base_create_klass_oop,经过这一步后大部分属性值都被赋空。

6.自指

执行完上面步骤后,返回到了klassKlass::create_klass()函数中,开始执行k->set_klass(k)这个逻辑,这里的k是klassOop,是klassOopDesc*类型指针,这一步其实是在设置klassOopDesc的_metadata变量,在之前的步骤中,_metadata被设置为NULL,而这一次则是将它指向了自己。

常量池klass模型(2)

constantPoolKlass模型构建

constantPoolKlass模型构建的过程与上文相同:

内存申请

内存清零

初始化_mark

初始化-metadata

初始化klass

返回包装好的klassOop

虽然常量池模型构建与klassKlass构建的基本逻辑一致,但是入参不同,而最后_metadata所指也不同,constantPoolKlass对应的oop的_metadata最终指向前面创建的klass,最终内存布局如下图:

e8fe26a78ec9292df5fa9c8cf2961c98.png

constantPoolOop与klass

其实constantPoolKlass正式constantPoolOop的类元信息,即constantPoolOop的_metadata指针应该指向constantPoolKlass对象实例,而JVM内部确实也这么做了:

constantPoolOop oopFactory::new_constantPool(int length,

bool is_conc_safe,

TRAPS) {

constantPoolKlass* ck = constantPoolKlass::cast(Universe::constantPoolKlassObj());

return ck->allocate(length, is_conc_safe, CHECK_NULL);

}

在这段逻辑中先是获取到了随JVM启动创建的constantPoolKlass实例指针ck,然后调用了ck->allocate()方法,ck指针最终被传递到post_allocation_install_obj_klass()中,因此JVM在执行obj->set_klass()方法时,constantPoolOop的_metadata指针便在此处指向了constantPoolKlass实例对象,最终构建的constantPoolOop内存布局如下图:

36dc349e18d0545cae41019d0a5b5cba.png

至此constantPoolOopDesc实例的构建便结束了,其他oop实例对象的构建大同小异。

klassKlass终结符

JVM在构建constantPoolOop过程中,由于它本身大小不固定,所以需要使用constantPoolKlass来描述不固定信息,而由于constantPoolKlass也是不固定的,因此会引用klassKlass来描述自己,但如果klassKlass也引用一个klass来描述自身,那么klass链路将一直持续,因此JVM将klassKlass作为整个引用链的终结符,并让他自己指向自己。

常量池解析

constantPoolOop域初始化

在构建constantPoolOop的过程中,会执行constantPoolKlass::allocate()函数,该函数干了三件事:

构建constantPoolOop对象实例

初始化constantPoolOop实例域变量

初始化tag

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);

// only set to non-zero if constant pool is merged by RedefineClasses

pool->set_orig_length(0);

// if constant pool may change during RedefineClasses, it is created

// unsafe for GC concurrent processing.

pool->set_is_conc_safe(is_conc_safe);

// all fields are initialized; needed for GC

//省略部分代码

}

初始化tag

在创建constantPoolOop过程中,会为其_tags域申请内存空间:

// initialize tag array

typeArrayOop t_oop = oopFactory::new_permanent_byteArray(length, CHECK_NULL);

typeArrayHandle tags (THREAD, t_oop);

for (int index = 0; index < length; index++) {

tags()->byte_at_put(index, JVM_CONSTANT_Invalid);

}

pool->set_tags(tags());

可以看到JVM在永久区为tags开辟了与constantPoolOop实例数据长度一致的空间,接下来JVM开始解析字节码文件的常量池元素,并逐个填充到这块区域。

解析常量池元素

现在开始解析常量池元素,为此专门定义了一个函数:parse_constant_pool_entries():

void ClassFileParser::parse_constant_pool_entries(constantPoolHandle cp, int length, TRAPS) {

// Use a local copy of ClassFileStream. It helps the C++ compiler to optimize

// this function (_current can be allocated in a register, with scalar

// replacement of aggregates). The _current pointer is copied back to

// stream() when this function returns. DON'T call another method within

// this method that uses stream().

ClassFileStream* cfs0 = stream();

ClassFileStream cfs1 = *cfs0;

ClassFileStream* cfs = &cfs1;

#ifdef ASSERT

assert(cfs->allocated_on_stack(),"should be local");

u1* old_current = cfs0->current();

#endif

// Used for batching symbol allocations.

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++) {

// Each of the following case guarantees one more byte in the stream

// for the following tag or the access_flags following constant pool,

// so we don't need bounds-check for reading tag.

u1 tag = cfs->get_u1_fast(); //从字节码文件中读取占1字节宽度的字节流,因为每一个常量池的起始元素的第一个字节用于描述常量池元素类型

switch (tag) { //对不同类型的常量池元素进行处理

case JVM_CONSTANT_Class :

{

cfs->guarantee_more(3, CHECK); // name_index, tag/access_flags

u2 name_index = cfs->get_u2_fast(); //获取类的名称索引

cp->klass_index_at_put(index, name_index);//将当前常量池元素的类型和名称索引分别保存到constantPoolOop的tag数组和数据区

}

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;

//省略部分代码

}

cp->klass_index_at_put(index, name_index);是将当前常量池元素保存到constantPoolOop中,其实现方式如下:

void klass_index_at_put(int which, int name_index) {

tag_at_put(which, JVM_CONSTANT_ClassIndex); //将当前常量池元素的类型保存到constantPoolOop所指向的tag对应位置的数组中

*int_at_addr(which) = name_index; //将当前常量池元素的名称索引保存到constantPoolOop的数据区中对应的位置

}

void tag_at_put(int which, jbyte t) { tags()->byte_at_put(which, t); }

当前常量池元素本身在字节码文件常量区位置索引将决定该元素最终在tag数组中的位置,也决定它的索引值最终在constantPoolOop的数据区的位置。

方法元素解析

Constant pool:

#1 = Methodref #10.#25 // java/lang/Object."":()V

#2 = Fieldref #9.#26 // JVM/Iphone6S.length:I

#3 = Fieldref #9.#27 // JVM/Iphone6S.width:I

#4 = Fieldref #9.#28 // JVM/Iphone6S.height:I

#5 = Fieldref #9.#29 // JVM/Iphone6S.weight:I

#6 = Fieldref #9.#30 // JVM/Iphone6S.ram:I

#7 = Fieldref #9.#31 // JVM/Iphone6S.rom:I

#8 = Fieldref #9.#32 // JVM/Iphone6S.pixel:I

#9 = Class #33 // JVM/Iphone6S

#10 = Class #34 // java/lang/Object

#11 = Utf8 length

#12 = Utf8 I

#13 = Utf8 width

#14 = Utf8 height

#15 = Utf8 weight

#16 = Utf8 ram

#17 = Utf8 rom

#18 = Utf8 pixel

#19 = Utf8 #20 = Utf8 ()V

#21 = Utf8 Code

#22 = Utf8 LineNumberTable

#23 = Utf8 SourceFile

#24 = Utf8 Iphone6S.java

#25 = NameAndType #19:#20 // "":()V

#26 = NameAndType #11:#12 // length:I

#27 = NameAndType #13:#12 // width:I

#28 = NameAndType #14:#12 // height:I

#29 = NameAndType #15:#12 // weight:I

#30 = NameAndType #16:#12 // ram:I

#31 = NameAndType #17:#12 // rom:I

#32 = NameAndType #18:#12 // pixel:I

#33 = Utf8 JVM/Iphone6S

#34 = Utf8 java/lang/Object

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, // JSR 292

JVM_CONSTANT_MethodType = 16, // JSR 292

//JVM_CONSTANT_(unused) = 17, // JSR 292 early drafts only

JVM_CONSTANT_InvokeDynamic = 18, // JSR 292

JVM_CONSTANT_ExternalMax = 18 // Last tag found in classfiles

};

我们仍然拿上面的字节码作为例子,第一个常量池元素为Methodref,因此tags的#1位置存的值是10,而constantPoolOop的数据区的#1位置有两个索引怎么存储呢?JVM对两个索引值进行了拼接,变成一个值然后存储:

void interface_method_at_put(int which, int class_index, int name_and_type_index) {

tag_at_put(which, JVM_CONSTANT_InterfaceMethodref);

*int_at_addr(which) = ((jint) name_and_type_index<<16) | class_index; // Not so nice

}

因此,#10.#25在constantPoolOop的数据区的#1位置实际存的是:0x0A19

字符串元素解析

无论是tag还是constantPoolOop的数据区,一个存储位置只能存放一个指针宽度的数据,而字符串往往很大,因此JVM专门设计了一个符号表的内存区,tag和constantPoolOop数据区内仅存指针指向符号区:

case JVM_CONSTANT_Utf8 :

{

cfs->guarantee_more(2, CHECK); // utf8_length

u2 utf8_length = cfs->get_u2_fast();

u1* utf8_buffer = cfs->get_u1_buffer();

assert(utf8_buffer != NULL, "null utf8 buffer");

// Got utf8 string, guarantee utf8_length+1 bytes, set stream position forward.

cfs->guarantee_more(utf8_length+1, CHECK); // utf8 string, tag/access_flags

cfs->skip_u1_fast(utf8_length);

// Before storing the symbol, make sure it's legal

if (_need_verify) {

verify_legal_utf8((unsigned char*)utf8_buffer, utf8_length, CHECK);

}

if (EnableInvokeDynamic && has_cp_patch_at(index)) {

Handle patch = clear_cp_patch_at(index);

guarantee_property(java_lang_String::is_instance(patch()),

"Illegal utf8 patch at %d in class file %s",

index, CHECK);

char* str = java_lang_String::as_utf8_string(patch());

// (could use java_lang_String::as_symbol instead, but might as well batch them)

utf8_buffer = (u1*) str;

utf8_length = (int) strlen(str);

}

unsigned int hash;

Symbol* result = SymbolTable::lookup_only((char*)utf8_buffer, utf8_length, hash);

if (result == NULL) {

names[names_count] = (char*)utf8_buffer;

lengths[names_count] = utf8_length;

indices[names_count] = index;

hashValues[names_count++] = hash;

if (names_count == SymbolTable::symbol_alloc_batch_size) {

SymbolTable::new_symbols(cp, names_count, names, lengths, indices, hashValues, CHECK);

names_count = 0;

}

} else {

cp->symbol_at_put(index, result);

}

}

break;

为了节省内存JVM会先判断符号表中是否存在相同的字符串,如果已经存在则不会分配内存,这就是你在一个类中定义两个字符串但是这两个字符串的值相同,最终都会同时指向常量池中同一个位置的原因。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值