JVM的学习分两个阶段即原理和源码学习、通过原理知识的学习我们能粗略的了解从java代码到字节码文件再到JVM运行产出的流程,之后你肯定会有一万个为什么,那时只能阅读源码来回答你的一万个为什么……
上篇文章学习了JVM内存区域,了解了JVM各个内存区域存储的数据,其中堆内存用来存储创建的对象,那下面一起来了解一下对象创建相关内容以及HSDB查看堆内存在JVM内存中的分配区域。
带着这些疑问开始new指令创建对象的学习……
1、创建的对象包含什么?
2、对象内存布局(通俗说对象的组成部分)是怎样的?
3、对象组成部分细分即对象头运行时数据包含哪些、元数据指针是什么、对象实体数据在哪里?
4、对象大小怎么算?
5、创建对象的流程是怎样的?
6、对象内存分配方式有哪些?
7、instanceKlass对象、Class对象、instanceOopDesc(instanceOop)对象、以及他们之间的关系?
8、对象是分配在堆内存,那堆内存在JVM内存中的具体地址在哪里?
一、创建的对象包含什么
1、其实说的具体点或者说更明确一点这里说的对象只包含对象成员变量;也就是我们定义的类中的对象成员变量;也就是对象数据体。
2、为何不包含对象头和填充数据,因为所有对象都有相同的对象头数据结构或者说是”相同“的对象头和填充数据。
3、对象头不是我们定义的数据是每个对象创建时自动添加的。
二、对象的内存布局
1、对象组成
2、对象内存布局
JVM内部使用instanceOopDesc 对象表示一个对象,instanceOop是instanceOopDesc别名。
3、对象大小计算
一般非数组对象,未开启压缩 | 8个字节对象头(mark) + 8字节对象指针 + 数据区 + padding内存对齐(按照8的倍数对齐) |
一般非数组对象,开启压缩 | 8个字节对象头(mark) + 4字节对象指针 + 数据区 + padding内存对齐(按照8的倍数对齐) |
OK 我们了解了对象是什么、对象的组成、对象的内存布局,那下面看看对象的创建流程……
三、对象创建流程
1、对象创建可以从不同角度来说如:语言、JVM、应用程序
JVM层面对象创建上面图形值整个流程,但是具体代码实现还是有一下不同,下面来看看JVM层面创建对象的具体流程。
2、JVM内部对象创建
JVM内部对象的创建分两类即快速分配和慢速分配,如果new 后面参数这个类已经被加载、解析、初始化过则进行快速创建否则慢速创建,这也正式我们程序中的两种情况即第一次创建该类对象和之后第二次……第N次创建该类对象。
2.1、创建对象流程图
3、快速分配
如果在实例分配之前已经完成了类型的解析,那么分配操作仅仅是在内存空间中划分可用内存,因此能以较高的效率实现内存分配,因为称为快速分配。
3.1、快速分配条件
// 确保常量池中存放的是已解释的类if (!constants->tag_at(index).is_unresolved_klass())// 确保对象所属类型已经经过初始化阶段if (ik->is_initialized() && ik->can_be_fastpath_allocated())
3.2、快速分配空间的两种选择策略
根据分配空间是来自于线程私有区域还是共享的堆空间,快速分配又可以分为两种空间选择策略。
选择TLAB:首先尝试在TLAB中分配,因为TLAB是线程私有区域,故不需要加锁便能够保证线程安全。在分配一个新的对象空间时,将首先尝试在TLAB空间中分配对象空间,若分配空间的请求失败,则在尝试使用加锁机制在Eden区分配对象。
选择Eden空间:若失败,则尝试在共享的Eden区进行分配,Eden区是所有线程共享区域需要保证线程安全,故采用CAS操作进行分配。若分配失败则再次尝试该操作直至分配成功为止。
3.3、实例数据空间进行填零
根据VM选项ZeroTLAB的配置,若为false,虚拟机接下来会对实例数据空间进行填零操作
因为填零操作所以对象的实例字段在java代码中可以不赋初始值就直接使用,访问到的变量的值就是默认的零值。
3.4、虚拟机对象头
3.4.1、设置Mark Word。
3.4.2、设置类型元数据指针:根据VM选项UseCompressedOops的配置,设置类型指针元信息_metadata._compressed_klass或_metadata._klass。
3.5、设置栈顶对象引用
待完成对象的空间分配和初始化后,就可以设置栈顶对象引用。
3.6、更新PC寄存器指令地址
4、上面是部分关键JVM实现对象分配信息,具体实现代码如下
解释器创建对象申请内存代码 :./hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp
Line: 1966
4.1、快速分配部分代码
// 确保常量池中存放的是已解释的类
if (!constants->tag_at(index).is_unresolved_klass()) {
// 断言确保是klassOop和instanceKlassOoop
oop entry = (klassOop) *constants->obj_at_addr(index);
assert(entry->is_klass(), "Should be resolved klass");
klassOop k_entry = (klassOop) entry;
assert(k_entry->klass_part()->oop_is_instance(), "Shold be instanceKlass");
instanceKlass* ik = (instanceKlass*) k_entry->klass_part();
// 确保对象所属类型已经经过初始化阶段
if (ik->is_initialized() && ik->can_be_fastpath_allocated()) {
// 取对象长度
size_t obj_size = ik->size_helper();
oop result = NULL;
// 记录是否需要将对象所有字段置零值
bool need_zero = !ZeroTLAB;
// 是否在TLAB中分配对象
if (UseTLAB) {
result = (oop) THREAD->tlab().allocate(obj_size);
}
if (result == NULL) {
need_zero = true;
// 直接在eden中分配对象
HeapWord* compare_to = *Universe::heap()->top_addr();
HeapWord* new_top = compare_to + obj_size;
// cmpxchg是x86中的CAS指令,这里是一个C++方法,通过CAS方式分配空间,并发失败的话,
// 转到retry中重试直至成功分配为止
if (new_top <= *Universe::heap()->end_addr()) {
if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to)
!= compare_to) {
goto retry;
}
result = (oop) compare_to;
}
}
if (result != NULL) {
// 如果需要,为对象初始化零值
if (need_zero) {
HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
obj_size -= sizeof(oopDesc) / oopSize;
if (obj_size > 0) {
memset(to_zero, 0, obj_size * HeapWordSize);
}
}
// 根据是否启用偏向锁,设置对象头信息
if (UseBiasedLocking) {
result->set_mark(ik->prototype_header());
} else {
result->set_mark(markOopDesc::prototype());
}
result->set_klass_gap(0);
result->set_klass(k_entry);
// 将对象引用入栈,
SET_STACK_OBJECT(resukt, 0);
// 更新PC寄存器值继续执行下一条指令
UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
}
}
四、JVM内部创建对象之后是和instanceOopDesc、instanceKlass 、Class对象什么关联关系,他们是怎么关联的
1、instanceKlass、Class建立关系
创建对象的过程需要经历类加载、连接、初始化然后才能创建对象,当类加载器加载class文件后会解析class文件并生成instanceKlass 对象也就是JVM内部生成加载的class对应的类,然后创建一个instanceKlass 对象的”java镜像”即Class对象,Class对象是被instanceKlass 对象引用即instanceKlass对象中的_java_mirror 这个指针引用堆内存中的Class对象…… 这样在加载完成之后instanceKlass对象和Class对象之间的关联关系确定了。
上一篇文章中也提到,JVM不想直接把instanceKlass这个内部的数据结构暴露给java程序所以按照instanceKlass对象创建了一个Class对象被称作是instanceKlass对象的”java镜像”。然后把这个”java镜像“ 暴露给java程序……
2、instanceOopDesc、instanceKlass建立关系
对象的组成包含三部分:对象头、对象实体(具体的对象数据)、补齐填充,其中对象头又包含两部分即对象运行时数据如对象hash值,对象gc年龄,锁,线程id等信息还有一部分叫对象类型指针,这个指针叫元数据指针,该指针指向的就是方法区中的instanceKlass对象。
其实创建完对象后instanceOopDesc中含有_meta_data 或_compress_meta_data 这样的指针变量这个变量就是对象头中的元数据指针变量,所指向的就是instanceKlass 对象。
OK 这样 instanceOopDesc 、instanceKlass 、Class三个对象之间的引用关系确定了。
五、对象在内存中的地址分配,使用jdb运行代码、HSDB分析具体内存分配
这里推荐两个学习JVM比较好的工具即jdb和HSDB,jdb是用来调试java代码。而HSDB用来查看JVM内部对象信息、以及对象在JVM内存中的地址、堆内存开始地址、结束地址信息以及指定内存地址空间内查询每种类型类的信息等……
1、jdb -classpath .:./bin TestMain 使用jdb调试TestMain 类文件的执行
2、sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB 启动HSDB
3、HSDB是查看某个时间点的快照信息,所以在获取快照的时候会出现程序阻塞情况所以线上勿用
4、使用jdb调试代码时编译的javac命令要添加一个-g参数否则无法调试;javac -g TestMain.java
5、使用jdb调试代码、使用HSDB查看JVM内部instanceOop对象信息
6、查看TestJdb类中的对象成员变量TestJdb jdbUse1 内存地址信息以及内存布局
六、VM配置参数
1、JVM参数配置
上面涉及到了两个VM配置参数即UseTLAB、UseCompressedOops 通过上面的学习也了解了他们的作用而且也了解了开启他们的优势,所以这两个参数在JVM参数配置中一定要配置……
2、网上一直讨论的new是创建对象还是构造函数创建对象还是两个一起创建对象?
三种都是对的也是错的,从语言、JVM、应用程序三个角度来看确实是对的。但是不说前提、不说是从哪个角度来看,以及具体的操作是什么那确实是没法确定!