对象是如何存放在堆空间中的

目录

1、对象的创建与内存分配

2、对象的内存布局

3、对象的访问定位


1、对象的创建与内存分配

当虚拟机遇到一条new字节码指令时,会先检查这个类型是否已经加载。如果还没有,就进行类加载过程

在类加载检查通过后,虚拟机将为新生对象分配内存。

一个对象所需的内存大小,在类加载完成后就可以确定下来,所以只需要把一块确定大小的内存从堆中划分出来。

1、如何从堆上划分内存

Java堆的内存管理有两种方式:

  • 指针碰撞:堆内存绝对规整,一边存储数据,一边空闲,中间用一个指针分隔。分配内存只需要挪动指针即可
  • 空闲列表:已使用的内存和空闲内存混在一起,虚拟机需要维护一个列表来记录哪些内存块是可用的。分配内存时从列表上划分。

虚拟机选择哪种内存管理方式,与Java堆是否规整有关。而堆是否规整,又取决于该虚拟机使用的垃圾回收器是否带有“空间压缩整理”功能决定。

  • 当使用Serial、ParNew这类带空间压缩整理的收集器,就采用指针碰撞,简单高效
  • 使用CMS这种基于清除(Sweep)算法的收集器,理论上就需要采用空闲列表来分配内存

2、划分内存如何保证并发安全

对象创建是虚拟机中非常频繁的操作,如果不作处理,很可能出现正在给A分配内存,还没有完成,B又来使用原先的内存状态分配内存的情况。

虚拟机使用两种方式确保线程安全:

  1. 对分配内存空间的动作进行同步。(具体是采用乐观锁+失败重试的方式)

  2. 把内存分配的动作按照线程分配在不同的空间进行。

    每个线程在堆中的Eden区预先分配一小块内存,作为“本地线程分配缓冲(TLAB)”,线程优先在自己的TLAB中分配内存,不够用了再进行同步。

虚拟机是否启用TLAB,通过这个参数来设定:

-XX:+/-UseTLAB

3、内存分配完成之后的工作

内存分配出来了,虚拟机对分配到的这部分空间进行处理,把除了对象头之外的地方都初始化为零值,接着填充对象头。

此时对虚拟机来说,对象就创建出来了。但这是一个空白对象,Java代码中的构造函数还未执行,所以对象还尚未初始化。

只有new指令执行完后,执行<init>()构造方法,一个可用的Java对象才被完整创建出来。

4、总结 Java对象的创建过程

Java对象的创建过程:

类型检查、分配内存、初始化零值、设置对象头、执行构造方法

  1. 遇到new关键字,先检查这个指令的参数是否能在常量池中找到该类型的符号引用。

  2. 如果找到了,检查这个类型是否已经完成加载并初始化

  3. 如果没有找到,说明类还没有加载,先进行类加载过程。

  4. 类加载的检查阶段通过后,这个类的对象需要占用的内存大小就已经确定了。JVM就会给对象分配内存

    • 分配内存涉及到三个细节:

      • 内存分配的两种方式:指针碰撞、空闲列表

      • 内存分配的线程安全:乐观锁+失败重试

      • 对于小对象,线程优先在堆中自己的“本地线程分配缓冲区 TLAB”上分配内存

        对于大对象,可以选择直接放入老年代

  5. 处理分配到的内存空间,把除了对象头之外的地方都初始化为零值,这样对象的成员变量就有默认值了

  6. 填充对象头,设置对象的类型、分代年龄、是否启用偏向锁。hashcode会在第一次调用时懒加载。

  7. 执行该对象的构造方法

然后就获得了一个可用的Java对象。

5、对象内存分配的基本策略

整体来说:

  1. 新对象优先在Eden区分配
  2. 大对象直接进入老年代
  3. 长期存活的对象将进入老年代

1、新对象优先在Eden区分配

如果Eden区空间不足,就会触发一次Minor GC。

  • 如果在新生代GC的期间,Eden区的存活对象很多,survivor区放不下,就会通过分配担保机制,把新生代的对象复制到老年代中。
  • 如果老年代的空间也不足,就会触发一次Full GC,这个比较耗时。

2、为什么大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。

有两方面考虑:

  • 可能新生代的Eden区内存空间不足,不得不提前触发一次GC。因为大对象有更大的概率会遇到内存不足的情况。
  • 以后的新生代GC,如果大对象存活了下来,suvivor区可能放不下,还是会通过分配担保机制进入老年代。所以可以选择直接放入老年代。

有一个参数:

-XX:PretenureSizeThreshold

大于这个数量直接在老年代分配。缺省为0 ,表示不会直接分配在老年代。

3、长期存活的对象将进入老年代

每个对象会保存一个分代年龄,每熬过一次GC,分代年龄就+1

当分代年龄超过阈值,就会晋升到老年代。

可以通过这个参数调整,默认15

-XX:MaxTenuringThreshold

HotSpot在这里用了动态分代年龄的机制,在分代收集理论中有记录。

2、对象的内存布局

在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

1、对象头

对象头部分包含两类信息:

  • mark word:用于存储对象自身的运行时数据,如哈希值、CG分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
  • klass word:类型指针,即对象指向它的类型元数据的指针,JVM通过这个指针来确定该对象是哪个类的实例。

如果对象是一个数组,对象头还必须存储它的长度,否则无法确定数组对象的大小。

2、实例数据

存放对象的有效信息,包括代码中定义的和从父类中继承的。存储顺序有两方面影响:

  • 代码中的书写顺序
  • 虚拟机分配策略

默认的分配策略是,相同宽度的字段会被分配到一起存放。以这个条件为前提,父类的变量会在子类的之前。

如果HotSpot开启这个参数,那么子类中的较窄变量可以允许插入父类变量的空隙中,节省一点空间

+XX:CompactFields:true //默认就为true

3、对齐填充

这部分起到占位符的作用。

HotSpot虚拟机的自动内存管理系统要求,对象起始地址必须是8字节的整数倍,所以任何对象的大小都必须是8字节的整数倍。

对象头的部分已经被精心设计成了8字节或16字节,而实例数据部分内容无法保证长度,不够8字节的地方,用对齐填充来补齐即可。

3、对象的访问定位

对象的访问定位方式是指,栈上的引用如何指向堆上的对象。

Java程序会通过栈上的 reference 数据来操作堆上的具体对象,这个 reference 类型只被固定成是一个引用,而没有指定实现方式。

所以虚拟机可以自由实现对象的访问方式。主流的方式有这两种:

  • 使用句柄访问:
    • Java堆中划分出一块内存作为句柄池
    • reference 中存储对象的句柄地址
    • 句柄中包含了对象的“实例数据”与“类型数据”各自具体的具体地址信息。
  • 使用直接指针访问:
    • reference 中直接存储对象的具体地址

两种方式各有优势:

  • 句柄访问的最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动时(比如发生垃圾回收),只会改变句柄中的实例数据指针,而不需要修改 reference 。
  • 直接指针访问的好处就是速度更快,节省了一次指针定位的开销。对象访问操作非常频繁,这也是HotSpot使用的方案。

具体的对象访问定位方式和GC的类型有关。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值