自动内存管理(2) - Java

承接上一文,上一文介绍了各个内存区域的信息。接下来是HotSpot虚拟机的对象分配、布局和访问。

  1. 对象的创建

        创建对象(例如克隆、反序列化)仅用一个new关键字就可,那么对象(讨论的是不同的Java对象,不包括数据对象和Class对象)的创建是怎么样的过程呢?
        虚拟机遇到new指令,首先看是否能到常量池中定位到该类的引用,并且检查这个类是否已经被加载、解析和初始化过。若有,则直接使用这个符号引用指向的类的实例,若没有,则必须执行相应的类加载过程。
        在类加载检查通过后,为新生对象分配内存,即从Java堆中划出一块确定大小的区域。此时有两种分配内存的方式:“指针碰撞”、“空闲列表”。

    1. “指针碰撞”:假如Java堆中内存是规整的,所有用过的内存放一边,没有用过的内存放一边,中间放着一个指针作为分界点的指示器。分配内存就是移动指针的操作。
    2. “空闲列表”:若Java堆中内存是不规整的,已使用的内存和未使用的内存相互交错,那虚拟机就必须用一个列表,记录上哪些内存是可用的,在分配内存时找到一块适合的空间划分给对象,再更新表上的记录。

        选择那种分配方式由Java堆是否规整确定,是否规整又由Java堆采用的垃圾收集器是否有压缩整理功能决定。在使用Serial、ParNew等带有Compact过程过程的收集器时,采用的是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器,通常采用空闲列表。
        除划分可用空间之外,还需要解决临界资源的问题。例如:正在给对象A分配内存,指针还未修改,对象B又使用原来的指针进行内存分配。解决这个问题,两个方法:

    1. 对分配内存空间的动作进行同步处理。
      实际上虚拟机采用CAS(Conmpare And Swap,比较和交换)配上失败重试的方式保障更新操作的原子性。点击此处查看CAS实现原理
    2. 把内存分配的动作按照线程划分在不同的空间进行。
      也就是说每个线程在Java堆中预先一小块分配内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),分配内存在TLAB上进行,等TLAB用完并分配新的TLAB时,需要进行同步锁定。是否使用TLAB,由参数-XX:+/-UseTLAB。

        内存分配完毕,需要将分配到的内存空间初始化为零值(不包括对象头),若使用TLAB,步骤可提前至TLAB中分配。这步骤保证了对象的实例属性可以在不符初始值的时候就可直接使用,程序可访问这些属性的数据类型对应的零值。接下来,虚拟机要对对象进行设置,包括该对象是哪个类的实例、如何找到该类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息放在对象的对象头(Object Header)中。

  2. 对象的内存布局

        在HotSpot虚拟机中,对象在内存中的布局分为对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

    1. 对象头
      对象头包含两部分:一部分是存储自身的运行时数据,例如哈希码、GC分代年龄、锁状态标志、线程持有的锁等,这些被成为“Mark Word”。它被设计为一个非固定的数据结构以便在极小的空间中存储尽量多的信息,会根据对象的状态复用自己的存储空间。另一部分是类型指针,用于指向该对象的类元数据,虚拟机通过该指针确定这个对象是哪个类的实例。但查找对象的元数据信息并不一定要经过对象本身。若对象为数组对象,则在对象头中还需要添加用于记录数组长度的数据,因为普通Java对象可以通过元数据信息确定Java对象的大小,但无法从数组的元数据确定数组的大小。
    2. 实例数据
      实例数据部分的是对象真正存储的有效信息。也是程序代码中程序员为属性赋的值。其存储顺序会受到分配策略参数(FieldAllocationStyle)影响。HotSpot默认的分配策略为long/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointer)。相同宽度的字段,在父类中定义的变量会出现在字类之前。如果CompactField参数为true(默认为true),则子类中较窄的变量可能会插入到父类变量的空隙中。
    3. 对齐填充
      由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。而对象头部分的数据正好是8字节的整数倍,因此,当对象实例数据部分没有对齐时,通过对齐填充来补全。
  3. 对象的访问定位

        Java程序通过栈上的reference数据来操作堆上的具体对象。但reference类型只规定了一个指向对象的引用,未定义该引用会通过何种方式定位、访问堆中的对象。故主流的访问方式有句柄和直接指针两种。

    1. 句柄
      Java堆中划分出一块区域作句柄池,reference中存储的是对象的句柄地址,而句柄中包含对象实例数据与类型数据各自的具体地址信息。
      如图下:
      在这里插入图片描述
    2. 直接指针
      如果使用直接指针访问,则Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息。而reference中存储的直接就是对象地址。
      在这里插入图片描述

    两种方法各有优势,使用句柄访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针。而直接指针访问方式最大好处是速度更快,节省了一定指针定位的时间开销,积少成多后也是非常可观的执行成本。

    对象在虚拟机的创建、布局和访问已经讲完了,楼主要考研了,可能没时间继续整理了,待以后再整理。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值