深入理解JVM - HotSpot虚拟机对象

对象创建

仅限于普通Java对象,不包括数组和Class对象;不包括复制(克隆?)、反序列化。

  1. 类加载检查:遇到字节码new指令,检查指令参数能否在常量池定位到一个类的符号引用,并检查这个符号引用对应的类是否已被加载、解析和初始化过,如果没有则执行对应的类加载过程
  2. 分配内存:对象所需内存大小在类加载完成后便可完全确定。
    1. 内存分配方式
      1. 指针碰撞:Bump The Pointer,要求Java堆内存绝对规整,使用过的内存放一边,未使用的放一边,中间指针隔开,内存分配仅是移动指针
      2. 空闲链表:Free List,Java堆内存不是绝对规整,则必须维护列表,记录那些内存可用,分配时从链表找到一块足够大的内存分配给对象实例
      3. 选择哪种分配方式由Java堆是否规整决定,Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定
      4. Serual、ParNew:带压缩整理过程的收集器,使用指针碰撞,简单高效
      5. CMS:基于清除(Sweep)算法,可先通过空闲列表得到大块分配缓冲区(Linear Allocation Buffer),然后在这个缓存区中使用指针碰撞的方式分配内存
    2. 线程安全
      1. 内存分配同步处理:CAS + 失败重试保证更新原子性
      2. 每个线程预先分配大块的TLAB(本地线程分配缓冲,Thread Local Allocation Buffer),只有这一步需要同步,为对象分配内存时在线程私有的TLAB中进行分配
  3. 对象内存初始化:将分配给对象的内存初始化为0值(但不含对象头,为什么?),如果使用了TLAB,这一步可提前至TLAB分配时执行(这里显然对对象头一同初始化了),保证对象实例字段不用赋初值即可使用。
  4. 对象头初始化
  5. 对象初始化:即构造函数的执行,由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中,对象在堆内存中的布局可分为三个部分:对象头、实例数据、对齐填充

对象头

  1. Mark Word:对象自身运行时数据,哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在32位和64位的虚拟机(未开启指针压缩)中分别32和64比特,官方称之为"Mark Word"。是一个动态数据结构,根据对象状态复用存储空间
  2. 类型指针:只想对象的类型元数据的指针,JVM通过它来确定对象是哪个类的实例,并非所有虚拟机都保留类型指针
  3. 数组长度:数组对象有

 

实例数据

  1. 无论是从父类继承下来的,还是子类中定义的字段,都必须记录下来
  2. 存储顺序受虚拟机分配策略参数(-XX:FieldsAllocationStyle)和字段源码定义顺序影响
  3. HotSpot默认分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),相同宽度存放在一起,在此前提下,父类定义变量在子类之前
  4. 若HotSpot虚拟机参数+XX:CompactFields参数值为true(默认就为true),那子类中较窄的变量也允许插入父类变量空隙之中,以节省空间

 

对齐填充

  1. 不是非必要的,仅起到占位符作用
  2. 这部分的存在是因为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说加上占位符,任何对象的大小都必须是8字节的整数倍
  3. 对象头刚好是8字节整数倍(1或2倍)

 

 

 

对象访问定位

Java程序通过栈上的reference操作堆上的具体对象。《JVM规范》仅规定了reference是一个指向对象的引用,并未规定这个引用该通过什么方式去定位、访问到堆中对象的具体位置。

主流访问方式主要有使用句柄和直接指针两种。

句柄

Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息

直接指针

Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销

 

 

直接指针:速度更快,它节省了一次指针定位的时间开销,HotSpot使用它

 

 

 

问题

  1. private的父类变量是否会被继承下来?父子类相同变量,父类方法访问,子类变量覆盖,那么子类调用访问的是哪个?子类重写方法,访问的是哪个?
  2. 一个对象最大为多大

 

posted @ 2022-02-13 23:23  YangDanMua  阅读(300)  评论(0编辑  收藏  举报