• JVM学习第八节 对象的实例化、内存布局与访问定位


    对象的实例化

        创建对象的方式
            1、new(最常见的方式)当构造起私有时,类可以提供静态方法或者Builder、Factory的静态方法调用。
            2、Class的newInstance():反射的方式,只能调用空参的构造器,权限必须时public。
            3、 Constructor的newInstance(Xxx):反射方式,可以调用空参、带参的构造器,权限也没有要求。
            v4、clone():不调用任何构造器,当前类需要实现Cloneable接口,实现clone()。
            5、反序列化:从文件中、网络中获取对象二进制流,从而还原成为对象。
            6、第三方库Objenesis
        从字节码角度简单梳理对象的创建与初始化的过程

    public class ObjTest {
    
        public static void main(String[] args) {
            Object o = new Object();
        }
        //      stack=2, locals=2, args_size=1
        //         0: new           #2                  // class java/lang/Object
        //         3: dup
        //         4: invokespecial #1                  // Method java/lang/Object."":()V
        //         7: astore_1
        //         8: return
        // 操作符new 判断一下对应的方法区中Object是否已经加载 如果没有加载需要使用类的加载器 加载java/lang/Object 在堆空间中申请空间
        // dup 复制一份 有两个对象一个在栈底用于赋值操作,另外一个作为句柄调用相关方法,
        // invokespecial 调用Object的""方法
        // astore_1保存本地变量表
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

        创建对象的步骤
            1、判断对象对应的类型是否加载、链接、初始化
                虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。(即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException 异常。如果找到,则进行类加载,并生成对应的Class 类对象。
            2、为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可(4个字节大小)。
                2.1、如果内存规整->指针碰撞:如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Pointer)来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact(整理)过程的收集器时,使用指针碰撞。
                2.2、内存不规整(比如CMS垃圾回收算法)
                    2.2.1、虚拟机需要维护一个列表
                    2.2.2、空闲列表分配:如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表(Free List)”。
                说明:选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
            3、处理并发安全问题:采用CAS失败重试、区域加锁保证原子性。每个线程预先分配一块TLAB,通过-XX:+/-UseTLAB参数设定。
            
            4、初始化分配到的空间–所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。
            5、设置对象的对象头:将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
            6、执行init方法进行初始化(显式初始化、代码块中初始化、构造器中初始化):在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。

    对象的内存布局

    在这里插入图片描述

    对象头(Header)

        主要是包含两个部分,1、运行时元数据(Mark Word),(哈希值、GC分代年龄、锁状态标志,线程持有的锁、偏向线程ID、偏向时间戳)2、类型指针,指向类元数据InstanceClass,确定该对象所属的类型。
        如果是数组还需要记录数组的长度。

    实例数据(Instance Data)

        说明:它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)。
        规则:1、相同宽度的字段总是被分配在一起。2、父类中定义的变量会出现在子类之前。3、如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙
        
        

    对齐填充(Padding)

        不是必须的,也没特别含义,仅仅起到占位符的作用,方便寻址(由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数,因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全)。
        举例解析代码

    public class Customer {
        
        int id = 1001;
    
        String name;
    
        Account acct;
    
        {
            name = "匿名用户";
        }
    
        public Customer() {
            this.acct = new Account();
        }
    }
    
    class Account {
    
    }
    
    class CustomerTest {
    
        public static void main(String[] args) {
            Customer cust = new Customer();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

        图示解析
    在这里插入图片描述

    对象的访问定位

        JVM是如何通过栈帧中的对象引用访问到其内部的对象实例?
    在这里插入图片描述
        1、为分配的堆空间中的地址值被赋值给了栈帧中的对象引用。通过栈上的reference访问。
        2、创建对象的目的是为了使用它。
        3、访问对象的方式主要有两种
            3.1 句柄访问在这里插入图片描述
            通过句柄访问的实现方式中,JVM堆中会划分单独一块内存区域作为句柄池,句柄池中存储了对象实例数据(在堆中)和对象类型数据(在方法区中)的指针。这种实现方法由于用句柄表示地址,因此十分稳定。 Java 堆中会分配一块内存作为句柄池。 句柄访问的优势:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改。

            3.2 直接指针(Hotspot使用)
    在这里插入图片描述
            通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。
         两者的比较
        使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。
        直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。
        如果是对象频繁 GC 那么句柄方法好。
        如果是对象频繁访问则直接指针访问好。

  • 相关阅读:
    【推荐系统】推荐系统-基础算法 冷启动、及深度学习在冷启动上的应用
    Solitidy - fallback 回退函数 - 2种触发执行方式
    YOLOV7改进:最新开源移动端网络架构 RepViT | RepViTBlock即插即用,助力检测 | 清华 ICCV 2023
    XGBOOST案例
    Linux 执行 shell 报错 $‘\r‘ 的解决办法
    Android AMS——创建Application(七)
    【WEB前端2024】3D智体编程:乔布斯3D纪念馆-第46课-使用json文件
    孙宇晨最新研判:加密货币将成为全球金融基础设施的一部分
    【从零开始玩量化13】quantstats:分析你的量化策略
    Mongo的数据操作
  • 原文地址:https://blog.csdn.net/GayFei/article/details/126540464