在结束了JVM虚拟机的学习之后,我们已经实现了初级的JVM虚拟机的理解,但是这个理解还不够深入,我们需要更加深入地理解JVM,在本篇,我将会利用空闲的晚上的时间大致浏览《JVM/深入理解Java虚拟机:JVM高级特性与最佳实践》(第三版)然后利用书中的详实描写了丰富我的JVM知识面及深入的细节理解,作为本系列的开篇,我将梳理对象创建过程中的细节
从JVM层次理解对象的创建
对象创建过程的大致回忆
首先我们想要创建一个对象,都要经历类加载过程,类加载的过程大致分为3步:ClassLoading、Linking、Initializing;Linking又被分为三小步,分别是verification、preparation、resolution,这个过程我们大致回忆即可,不展开。
当一个类加载完毕后,同时在方法区内存会同步地创建一个这个类对象的实例(class obj)
接下来就进入到了对象创建的过程了,我们直接上JVM的汇编指令
new、dup、invokespecial、pop,今天,我们要详细讲解的就是这个new指令
new的调用与JVM层次的对象创建
new在调用的时候,首先会校验是否加载了类,如果加载了类,那么就会开始进行对象分配
对象的分配有两种模式,这两种模式和堆空间有关
假设我们的堆空间中被整理的非常干净,整个堆空间中有一个指针,在这个指针的左边全部都是已经被分配完毕的对象;指针的右边都是还没有分配的空间。
当我们需要分配一个对象空间的时候,其实就是将指针从左到右移动对象大小的距离,这就完成了对象的分配,这种分配模式叫做指针碰撞(Bump the Pointer)
如果我们的堆空间不是规整的,这个堆空间中有很多的碎片空间,我们的指针开始移动的时候指针移动的下一个空间不一定是一个空闲的位置,这个时候,我们想要真正的分配一个空闲空间,那么我们就需要维护一张表,这个表叫做 空闲列表 (free list)通过这个表我们就可以知道应该在哪里放置新的对象
关于这两种不同的分配对象的方式,我们需要知道的是这是因为我们选择的垃圾回收器造成的,首先我们要介绍基础垃圾回收器的算法
- serial、serial old、parNew、parallel old:使用mark compact算法
- parallel scavenge:使用copying算法
- CMS:使用mark sweep算法
如果使用的是mark compact算法或者copying算法实现的,那么对象分配的方法就是 指针碰撞
如果使用的是mark sweep算法,那么对象分配的方法就是 空闲列表
当然,敏锐的人已经发现了,如果我们在多线程环境下进行对象分配的时候,就有可能出现数个线程争用指针来分配空间的现象,如果没有同步机制,哪怕我们使用同一个指针来分配对象仍然会面对线程不安全问题。为了解决这个问题,HotSpot提供了一个结构来优化——TLAB
Thread Local Allocation Buffer,线程本地分配缓存区,这个缓存区在线程创建的时候就已经被创建了,每一个TLAB都存在于eden区域,默认占eden区域的1%,通过这个机制就可以避免大量线程争用对象创建资源的时候面临的线程不安全问题,大家就可以在自己的TLAB区域分配对象空间,节约了时间成本。
TLAB的缺点其实也可以想得到,每一个线程分配自己的空间,那么空间的利用率就会比不使用TLAB的时候要来的低
以上的过程就是在调用了JVM汇编 new 指令之后发生的事情,当JVM汇编指令new执行完毕的时候,从JVM的角度看来,一个对象就已经被成功创建了。但是在Java程序看来这才是一个对象创建的开始,因为还没有调用 invokespecial指令(调用对象的构造方法)
以下是HotSpot虚拟机的源码
if (!constants->tag_at(index).is_unresolved_klass()) { // 断言确保是klassOop和instanceKlassOop(这部分下一节介绍) 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(), "Should 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中分配对象 retry: 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); // 将对象引用入栈,继续执行下一条指令,这里就和dup指令有联系了 SET_STACK_OBJECT(result, 0); UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1); } } }
文档信息
- 本文作者:JunHua yin
- 本文链接:https://yin-JH.github.io/2021/04/05/%E9%98%85%E8%AF%BB-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA-%E8%A1%A5%E5%85%85JVM%E7%9F%A5%E8%AF%86%E4%BD%93%E7%B3%BB%E7%B3%BB%E5%88%97(%E4%B8%80)%E4%B9%8B%E5%AF%B9%E8%B1%A1%E5%88%9B%E5%BB%BA%E8%BF%87%E7%A8%8B%E7%9A%84%E8%A1%A5%E5%85%85/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)