一、对象的内存布局

Java对象的内存布局可以划分为三个部分:对象头、实例数据、对齐填充。
深入浅出JVM —— 对象

  1. 对象头: 包括两个主要部分,一是对象的运行时数据(也叫Mark Word),如hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向锁id、偏向时间戳等;二是对应的类型指针,指向它的类型元数据,虚拟机可以通过它确定该对象是哪个类的实例。另外如果是数组对象,还会有一个部分存放数组长度length,所以写代码是数组是.length,而其他的大部分都是.length()。
  2. 实例数据: 顾名思义就是我们在代码中定义的各种类型的字段内容(包括父类的)。
  3. 对齐填充: 只是为了补齐,比如HotShot虚拟机规定对象起始地址必需是8字节的整数倍,那实例数据分配完还不够就必需补齐了。

二、对象的创建

Java中最常见的对象创建方法便是通过new关键字创建对象了,一般会通过以下流程完成整个对象的创建。
深入浅出JVM —— 对象

  1. 加载类信息: 想要创建对象,必然得有对象所对应类的信息,所以虚拟机会先检查类信息是否已经加载,没有则加载(类的加载会做后面说),有则按照类信息开始往下执行。
  2. 内存分配: 对象的存放是需要空间的(在Java内存结构中说过对象是分配到堆中的)。目前有两种分配方式,一是指针碰撞(内存规整情况下),通过指针移动对象大小的距离,一边表示已分配的空间,一边表示未分配的空间;二是空闲列表(内存不规整情况下),通过维护一个保存有未分配空间信息的列表,按需划分空间给对象。可以看出用哪种方式取决于内存规不规整,而内存规不规整取决于GC所采用的垃圾回收算法,而垃圾回收器是垃圾回收算法的实现,所以通过JVM采用的垃圾回收器就可以判断程序所使用的是什么样的分配方式。

因为堆是线程共享的,那么在多线程情况下,必然会遭遇资源竞争问题。
为了解决多线程的竞争问题,有两种可选方案:
一是对分配的空间进行同步处理,虚拟机采用的是CAS配上失败重试的方式;
二是本地线程分配缓冲TLAB,即先给每个线程一块内存空间,线程在自己拥有的空间里分配,直到自己的空间不足,再进行同步获取新的缓冲内存。

  1. 初始化内存空间为0: 分配好空间后虚拟机会将分配的空间(除对像头外)初始化为0,这样对象的实例字段不需要赋初始值就能够使用了。
  2. 设置对象头: 给对象头的信息设置成正确的数值。
  3. 根据<init>()方法初始化对象: 也即是根据构造函数,将对象的实例数据设置为正确的值。至此,一个真正可用的对象就被构造处来了。

三、对象怎么定位

创建完对象,对象的引用reference会被保存在方法栈帧中的本地变量表中,reference对象具体地址有两种方式:

  1. 句柄访问:堆中开辟一个句柄池,每个句柄包含对象实例数据的指针和对象类型数据的指针,reference则指向这个句柄。好处是对象实例数据位置变动不会影响reference,缺点就是需要额外的空间维护句柄池。
    深入浅出JVM —— 对象
  2. 直接指针访问:reference直接指向对象实例数据,对象保存有类型数据的指针(和上面讲的对象内存布局相符)。好处是速度更快,少了一次指针定位的开销。
    深入浅出JVM —— 对象

结尾
该系列是为了理清脉络,抓住重点,记录让我灵机一动的点,故而采用了忽略细节、只讲关键的形式。
内容来源于《深入理解Java虚拟机》,有兴趣的自行阅读以便进一步深入理解其中细节。
如有错误,欢迎指正。

相关文章: