深入理解JVM - HotSpot虚拟机对象
对象创建
仅限于普通Java对象,不包括数组和Class对象;不包括复制(克隆?)、反序列化。
- 类加载检查:遇到字节码new指令,检查指令参数能否在常量池定位到一个类的符号引用,并检查这个符号引用对应的类是否已被加载、解析和初始化过,如果没有则执行对应的类加载过程
- 分配内存:对象所需内存大小在类加载完成后便可完全确定。
- 内存分配方式
- 指针碰撞:Bump The Pointer,要求Java堆内存绝对规整,使用过的内存放一边,未使用的放一边,中间指针隔开,内存分配仅是移动指针
- 空闲链表:Free List,Java堆内存不是绝对规整,则必须维护列表,记录那些内存可用,分配时从链表找到一块足够大的内存分配给对象实例
- 选择哪种分配方式由Java堆是否规整决定,Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定
- Serual、ParNew:带压缩整理过程的收集器,使用指针碰撞,简单高效
- CMS:基于清除(Sweep)算法,可先通过空闲列表得到大块分配缓冲区(Linear Allocation Buffer),然后在这个缓存区中使用指针碰撞的方式分配内存
- 线程安全
- 内存分配同步处理:CAS + 失败重试保证更新原子性
- 每个线程预先分配大块的TLAB(本地线程分配缓冲,Thread Local Allocation Buffer),只有这一步需要同步,为对象分配内存时在线程私有的TLAB中进行分配
- 内存分配方式
- 对象内存初始化:将分配给对象的内存初始化为0值(但不含对象头,为什么?),如果使用了TLAB,这一步可提前至TLAB分配时执行(这里显然对对象头一同初始化了),保证对象实例字段不用赋初值即可使用。
- 对象头初始化
- 对象初始化:即构造函数的执行,由new指令后是否跟随invokespecial指令决定(一般编译器会在new关键字处同时生成两个字节码指令,但其他方式产生的则不一定)
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); \ // 将对象引用入栈,继续执行下一条指令 SET_STACK_OBJECT(result, 0); UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1); } } }
对象内存布局
HotSpot中,对象在堆内存中的布局可分为三个部分:对象头、实例数据、对齐填充。
对象头
- Mark Word:对象自身运行时数据,哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在32位和64位的虚拟机(未开启指针压缩)中分别32和64比特,官方称之为"Mark Word"。是一个动态数据结构,根据对象状态复用存储空间
- 类型指针:只想对象的类型元数据的指针,JVM通过它来确定对象是哪个类的实例,并非所有虚拟机都保留类型指针
- 数组长度:数组对象有
实例数据
- 无论是从父类继承下来的,还是子类中定义的字段,都必须记录下来
- 存储顺序受虚拟机分配策略参数(-XX:FieldsAllocationStyle)和字段源码定义顺序影响
- HotSpot默认分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),相同宽度存放在一起,在此前提下,父类定义变量在子类之前
- 若HotSpot虚拟机参数+XX:CompactFields参数值为true(默认就为true),那子类中较窄的变量也允许插入父类变量空隙之中,以节省空间
对齐填充
- 不是非必要的,仅起到占位符作用
- 这部分的存在是因为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说加上占位符,任何对象的大小都必须是8字节的整数倍
- 对象头刚好是8字节整数倍(1或2倍)
对象访问定位
Java程序通过栈上的reference操作堆上的具体对象。《JVM规范》仅规定了reference是一个指向对象的引用,并未规定这个引用该通过什么方式去定位、访问到堆中对象的具体位置。
主流访问方式主要有使用句柄和直接指针两种。
句柄
Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
直接指针
Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
直接指针:速度更快,它节省了一次指针定位的时间开销,HotSpot使用它
问题
- private的父类变量是否会被继承下来?父子类相同变量,父类方法访问,子类变量覆盖,那么子类调用访问的是哪个?子类重写方法,访问的是哪个?
- 一个对象最大为多大