Skip to content

Java 虚拟机对象的创建、布局和访问

Published: at 16:37:41

主要讨论虚拟机在Java堆中对象的分配、布局和访问全过程

对象的创建

虚拟机收到一条new指令时, 首先将会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并检查这个引用代表的类是否已经被加载、解析和初始化过。如果没有, 必须先执行类加载的过程。

类加载通过后, 虚拟机会为新生对象分配内存, 对象所需的内存大小在类加载完成后就可以完全确定下来, 为对象分配空间等同于从JAVA堆中划分一块确认大小的内存, 分配的方式有两种:

  1. 指针碰撞(Bump the Pointer)

    假设JAVA堆中的内存是规整的, 所有用过的内存放在一边, 空闲可用的内存在另一边, 中间放一个指针作为分界线的指示器, 那么分配内存就是把指针往空闲区域挪动一段和新生对象大小相等的距离

    Serial、ParNew采用的指针碰撞

  2. 空闲列表(Free List)

    如果JAVA堆中的内存是不规整的, 虚拟机就必须要维护一个列表, 用来记录哪一块内存是可用的, 在分配内存的时候再从列表中找一块足够大的空间分配给新生对象, 并更新列表上的记录。

    CMS 收集器采用的空闲列表

并发情况下, 可能出现在给A分配内存, 指针还没来得及修改, 对象B又使用了原来的指针来分配内存的情况, 解决这问题, 有两种方案:

一是对分配内存空间的动作进行同步处理——实际上虚拟机才用的CAS加上失败重试的方式保证原子性;

二是把内存分配的动作按照线程划分不同的空间之中进行, 即每个线程在JAVA堆中预先分配了一块小的空间——本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。只有当TLAB空间用完需要分配新的TLAB的时候, 才需要同步锁定。虚拟机是否使用TLAB, 可以通过参数 -XX:+/-UseTLAB来设置。

内存分配完成后, 虚拟机会将内存空间都初始为0值(不包括对象头)。

接下来, 虚拟机要对对象进行必要的设置, 比如这个对象是哪个类的实例、如何才能找到元数据信息、对象的哈希码、对象的GC分代年龄信息等——这些信息都是存放在对象的对象头(Object Header)中。

从虚拟机的角度看, 上面的工作完成, 一个新的对象就产生了。

但是从程序员的角度看, 对象的创建才刚刚开始——方法还没有执行, 所有的字段都还是0, 执行指令new后会执行方法, 把对象按照程序员的意思进行初始化, 这样一个真正可用的对象才算完全产出。

对象的内存布局

对象在内存中存储的布局可以分为三个部分: 对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

对象的访问定位

创建对象后要使用对象, 我们JAVA程序需要通过栈上的reference 数据来操作JAVA堆上的具体对象, 但是JAVA虚拟机规范只规定了一个指向对象的引用, 并没有说明这个引用应该通过何种方式去访问JAVA堆中的对象, 所以这取决于虚拟机的具体实现。

目前主流的有使用句柄和直接指针两种方式:

  1. 使用句柄访问

    使用句柄的话, JAVA堆中会分配出一块内存来作为句柄池,**reference中存储的就是对象的句柄地址**,句柄中就包含了到对象具体实例数据地址指针和到对象类型数据地址指针(在方法区中)。

    untitled.jpg

  2. 使用直接指针访问

    如果是直接访问,reference中存储的就是对象地址

    untitled

句柄访问的最大优势就是reference中存储的是句柄地址,在对象被移动后只会改变句柄中的实例数据指针,reference本身不需要修改。

直接访问的优势就是速度更快,它节省了一次指针定位的开销。对象访问频繁,开销极少成多也是一项不小的成本。Sum HotSpot 使用的是直接访问。

JAVA 对象创建和引用关系