对象实例化、内存布局和访问定位

导读:本篇文章讲解 对象实例化、内存布局和访问定位,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

对象的实例化

创建对象的方式

  1. new 方式
  • 最常见的方式
  • 静态方法中调用new
  • xxxbuild/xxxFactory的静态方法
  1. Class的newInstance():反射的方式,只能调用空参的构造器,构造器的权限必须是Public
  2. Constructor的newinstance(…args):反射的方式,可以调用空参、带参的构造器、权限没有要求
  3. 使用clone():不调用任何构造器,类型需要实现Cloneable接口,实现clone()方法。
  4. 使用反序列化:从文件中,网络中获取一个对象的二进制流,将二进制流还原成对象实例。
  5. 第三方库Objenesis

创建对象的步骤

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

对象创建步骤总结
加载类元信息 -> 分配对象实例内存 -> 处理并发问题 -> 对象属性默认初始化(零值初始化) -> 对象头设置(关联类元信息) -> 对象属性显示初始化

对象的内存布局

对象头(Header)

  1. 运行时元数据(mark work)
    • 哈希值(HashCode)
    • GC分代年龄
    • 锁状态标志
    • 线程持有的锁
    • 偏向线程ID
    • 偏向时间戳
  2. 类型指针
    • 执行类元数据InstanceClass(元空间中对象所属的Class),确定该对象所属的类型
  3. 如果是数组,还需记录数组的长度

实例数据(Instance Data)

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

对齐填充(Padding)

不是必须的,也没有特别含义,仅仅起到占位符的作用

小结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

对象访问定位

jvm是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?
在这里插入图片描述
堆空间的对象的地址值赋给了栈帧中的对象引用,同时堆空间的对象的元数据指针指向方法区中的类型信息(Class)

方法定位详情

  1. 创建对象的目的是为了使用它
  2. JVM是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?
  • 定位,通过栈帧上的reference访问
  1. 对象访问方式主要有两种
  • 句柄访问
    在这里插入图片描述
  • 优缺点:
    优点:栈帧中的地址(reference)相对稳定,不会因为垃圾回收时对象位置移动而变得,因为句柄池中的地址改变就可以了;缺点:浪费内存,需要单独开辟一块句柄池空间,同时访问效率相对与直接访问较低,因为需要找到句柄池中的地址才能访问到对象数据实例和对象的类型信息。
    • 直接指针(Hotspot采用):
      1. 图示
        在这里插入图片描述
    1. 优缺点
      优点:访问效率高,reference直接就可以定位到对象数据实例;节省内存不用单独开辟句柄池来管理地址。
      缺点:栈帧中的地址会频繁变动,因为垃圾回收时对象的地址改变,栈帧中的引用变量的值也要跟踪改变。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100427.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!