• 深入理解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. 一个对象最大为多大
  • 相关阅读:
    [Linux Sets] hosts, wlan and other net-rele
    [Source] 温柔的图片
    [CATARC_2017] 第三周 & 残四周
    [CATARC_2017] 第二周
    ubuntu 安装
    知规矩者混天下言。
    python+selenium的web自动化测试之二(Jenkins自动执行)
    python+selenium的web自动化测试之一(手工执行)
    Python 入门小实例笔记
    Web安全测试工具 Burp Suit 使用简介
  • 原文地址:https://www.cnblogs.com/chenxingyang/p/15890869.html
Copyright © 2020-2023  润新知