JVM—对象的创建流程与内存分配

对象创建的流程图如下:

对象的创建流程图

对象的内存分配方式

内存分配的方式有两种:

  • 指针碰撞(Bump the Pointer)
  • 空闲列表(Free List)
分配方式 说明 收集器
指针碰撞(Bump the Pointer) 内存地址是连续的(新生代) Serial和ParNew收集器
空闲列表(Free List) 内存地址不连续(老年代) CMS收集器和Mark-Sweep收集器

内存分配方式1

内存分配方式2

指针碰撞

指针碰撞示意图如下:

指针碰撞示意图

内存分配安全问题:

虚拟机给A线程分配内存的过程中,指针未修改,此时B线程同时使用了该内存,就会出现问题。

解决方式:

  • CAS乐观锁:JVM虚拟机采用CAS失败重试的方式保证更新操作的原子性;
  • TLAB (Thread Local Allocation Buffer)本地线程分配缓存,预分配。

分配主流程

首先从TLAB里面分配,如果分配不到,再使用CAS从堆里面划分。

对象如何进入老年代

对象进入老年代流程如下:

对象如何进入老年代

  • 新对象大多数默认都进入Eden;

  • 对象进入老年代的四种情况:

    • 年龄太大 MinorGC15次 -XX:MaxTenuringThresHOLD 】;

    • 动态年龄判断:MinorGC后会动态判断年龄,将符合要求对象移入老年代;

      MinorGC之后,发现Survivor区中的一批对象的总大小大于了这块Survivor区的50%,那么就会将此时大于等于这批对象年龄最大值的所有对象,直接进入老年代。

      例子: Survivor区中有一批对象,年龄分别为年龄1+年龄2+年龄n的多个对象,对象总和大小超过了Survivor区域的50%,此时就会把年龄n及以上的对象都放入老年代。希望那些可能是长期存活的对象,尽早进入老年代。
      比率可以由-XX:TargetsurvivorRatio指定
      
    • 大对象直接进入老年代1M【 -XX:PretenureSizeThreshold 】;(前提是Serial和ParNew收集器)

      为了避免大对象分配内存时的复制操作降低效率。

      避免了Eden和Survivor区的复制。

    • MinorGC后存活对象太多无法放入Survivor。

空间担保机制

空间担保机制:当新生代无法分配内存的时候,我们想把新生代的老对象转移到老年代,然后把新对象放入腾空的新生代。此种机制我们称之为内存担保。

空间担保流程图如下:

空间担保流程图

对象内存布局

对象内存布局示意图如下:

对象内存布局

对象里的三个区

堆内存中,一个对象在内存中存储的布局可以分为三块区域:

  • 对象头(Header) : Java对象头占8byte。如果是数组则占12byte。因为JVM里数组size需要使用4byte存储。

    • 标记字段MarkWord:

      • 用于存储对象自身的运行时数据,它是synchronized实现轻量级锁和偏向锁的关键。

      • 默认存储:对象HashCode、GC分代年龄、锁状态等等信息。

      • 为了节省空间,也会随着锁标志位的变化,存储数据发生变化。

        锁标志位的变化

    • 类型指针KlassPoint:

      • 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
      • 开启指针压缩存储空间4byte,不开启8byte。
      • JDK1.6+默认开启
    • 数组长度:如果对象是数组,则记录数组长度,占4个byte,如果对象不是数组则不存在。

    • 对齐填充:保证数组的大小永远是8byte的整数倍。

  • 实例数据(Instance Data):生成对象的时候,对象的非静态成员变量也会存入堆空间

  • 对齐填充(Padding) :JVM内对象都采用8byte对齐,不够8byte的会自动补齐。

如何访问一个对象

有两种方式:

  1. 句柄:稳定,对象被移动只要修改句柄中的地址

  2. 直接指针:访问速度快,节省了一次指针定位的开销

句柄方式访问对象

通过直接指针访问对象

热门手游下载
下载排行榜