【JVM】java对象探究

176 阅读8分钟

1. 对象的创建

  • 当虚拟机遇到一条字节码new的指令时:
    • 加载类:首先检查这个指令的参数能否在常量池【jdk8中为元空间】中定位到一个类的符号引用,并检查这个符号代表的类是否已被加载,解析和初始化过。如果没有必须先执行类的初始化过程
    • 分配内存:在类加载检查过后,为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定。
    • 初始化:内存分配完成后,虚拟机必须将分配到的内存空间(不包括对象头)都初始化为零值,如果使用了TLAB的话,初始化工作可提前至TLAB分配时顺便进行。保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段数据类型对应的零值
    • 设置对象头:JVM对对象进行必要设置:比如对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(对象的哈希码会延后到真正调用Object::hashCode()才计算)、对象的GC分代年龄信息。这些信息存放在对象的对象头中。根据虚拟机当前运行状态不同(是否使用偏向锁等)对象头会有不同的设置方式。
    • 执行构造函数进行初始化:上面4步从虚拟机的视角看,一个新的对象已经产生。但是从程序的视角看,对象创建才刚刚开始-构造函数还没执行,所有的字段都为默认的初始值。对象需要的其他资源和状态信息还没有按照预定的意图构造好。

2. 内存分配的方式

虚拟机堆内存分配考虑2个问题:堆内存是否规整、分配内存是否线程安全。

2.1 按照是否规整分类

指针碰撞:假设Java堆内存是绝对规整的

  • 所有被使用的放在一边,未使用的也放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把指针向着空间空间的方向挪动一段与对象大小相同的距离

空闲列表:如果java堆中的内存不是规整的

  • 虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配内存时从列表中找一块足够大的空间划分给对象实例,并更新列表上的记录

选择哪分配方式取决于Java堆内存是否规整,而是否规整又与垃圾回收期是否具备空间压缩整理的能力决定。

  • Serial,ParNew等带压缩整理过程的收集器,使用指针碰撞
  • CMS基于标记清除算法的收集器,理论上使用复杂的空闲列表来分配内存(通过空闲列表拿到一块大的分配缓冲区后仍然可以使用指针碰撞)

2.2 并发安全

如果是指针碰撞,在创建对象内存分配十分频繁的条件下,仅仅修改指针所指向的位置,在并发的情况下也不是线程安全的。有两种解决方案:

  • 对分配内存空间的动作进行同步处理:实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
  • 把内存分配的动作按照线程划分在不同的空间进行:每个线程在Java堆中预先分配一小块内存,称为TLAB(线程本地分配缓冲),哪个线程要分配内存,就在哪个线程的的本地缓冲区中分配,只有本地缓冲区使用完了,分配新的缓冲区时,才需要同步锁定。
-XX:+UseTLAB   //是否使用TLAB,可以通过参数来设定
-XX:-UseTLAB   //不适用TLAB

3. 对象的内存布局

在HOTSPOT虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据、对齐填充

对象头 主要分为两类信息:

  • 用于存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程id、偏向时间戳的。在64位虚拟机中(未开启指针压缩)占用64B(8字节);对象需要存储的运行数据很多其实已经超越了64能记录的最大限度,但对象头里面的信息是与对象定义的数据无关的额外存储成本,mark word被设计成有着动态定义的数据结构。
存储内容标志位状态
对象HashCode,对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记
偏向线程id,偏向时间戳,对象分代年龄01可偏向
  • 类型指针:指向它的类型元数据的指针,java虚拟机通过这个指针确定该对象是哪个类的实例。
    • 并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说:查找对象的元数据信息不一定要通过对象本身
    • 如果对象是一个Java数组:在对象头上还必须有一块用于记录数组长度的数据;因为虚拟机可通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组长度不确定,将无法通过元数据的信息推断数组大小

实例数据:是对象真正存储的有效信息

  • 在程序代码片段中定义的各种数据类型的字段内容,无论是从父类中继承、还是子类中定义都必须记录下来。存储顺序受到参数-XX:FieldsAllocationStyle=0|1和定义顺序有关.
  • hotspot默认分配顺序为:long/double,int/float,short/char,byte/boolean,oops.可以看出,相同宽度的字段总是被分配到一起存放,在满足这个前提条件下,父类定义的变量会出现在子类之前。如果-XX:CompactFields=true参数值为true(默认为true)那子类中较窄的变量也允许插入父类变量的空隙之中。

对齐填充:仅仅起到占位符的作用 由于hospot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,对象头部分已经被设计为8字节的整数倍,因此如果对象实例部分的数据没有对齐,则需要对齐填充来补全

代码验证

  • POM依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-cli</artifactId>
    <version>0.6</version>
</dependency>
  • 代码
import org.openjdk.jol.info.ClassLayout;

public class ObjectDemo {
    public static void main(String[] args) {
        User user = new User();
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
        System.out.println(user.hashCode());//执行完hashCode方法后
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
}

class User {
    private long id;//8字节
    private int age;//4字节
    private short card1;//2字节
}
  • User变量占用8+4+2=14字节
  • 对象头markword 8字节+类型信息4字节=12字节
  • 对齐填充:32(8的整数倍)-12-14=6字节 执行结果:
com.xxd.demo.User object internals:
 OFFSET  SIZE  TYPE DESCRIPTION                    VALUE
      0     4       (object header)                01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4       (object header)                00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4       (object header)                43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4   int User.age                       0
     16     8  long User.id                        0
     24     2 short User.card1                     0
     26     6       (loss due to the next object alignment)//对齐填充
Instance size: 32 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total

1845066581
com.xxd.demo.User object internals:
 OFFSET  SIZE  TYPE DESCRIPTION                    VALUE
      0     4       (object header)                01 55 7b f9 (00000001 01010101 01111011 11111001) (-109357823)
      4     4       (object header)                6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
      8     4       (object header)                43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4   int User.age                       0
     16     8  long User.id                        0
     24     2 short User.card1                     0
     26     6       (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total

4. 对象的访问定位

创建完对象之后自然为了后续使用对象,访问对象的方式由虚拟机自行实现,分为:句柄访问直接指针

句柄访问

  • java堆将可能划分一块内存作为句柄池,reference中存的就是对象的句柄地址,而句柄中包含了对象实例数据类型数据各自的地址信息
  • 好处:
    • reference存放的是局稳定的句柄地址没在对象被移动(一般指是垃圾回收过程中移动)只会改变句柄池中实例数据指针,而reference本身不需要修改

直接指针【Hotspot使用】

  • java堆中对象的内存布局必须考虑如何存放访问的类型数据相关信息,reference存储的直接就是对象的地址
  • 好处:
    • 如果只是访问对象本身,不需要多一次间接访问的开销,访问速度更快