定位内存的对象
java 程序需要通过栈上的 reference 数据来操作堆上的具体对象。由于reference 类型在JVM 规范中只规定了一人指向对象的引用,并未定义这个引用如何定位和访问具体位置,所以对象访问方式由具体虚拟机实现而定。目前主流的访问方式有句柄和直接指针两种。
- 直接指针,java堆对象的布局中就必须考虑如何放置访问方法区中类型数据的相关信息。
- 句柄访问,java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例和类型数据各自的具体地址信息。
二者各有优势,使用句柄访问这样做的好处是中 reference 存储的句柄地址较为稳定,因为在Java 堆中进行了垃圾回收,对象的地址发生了改变的时候,只需要修改句柄的对象实例数据指针就行。而使用直接指针的最大好处就是速度更快。
对象在内存
对象在堆内存的内存布局主要有三部分,即对象头、实例数据以及对其填充。
对象头
对象头主要包含两部分的内容,一个叫做运行时元数据(MarkWord),一个叫做类型指针 (CIass MetadataAddress)。
类型指针指向元数据区代表当前类的 class 对象,确定该对象所属的类型,而运行时元数据又包含了:
- 哈希值(hashcode)也就是对象在堆空间中都有一个首地址值,栈空间的引用根据这个地址指向堆中的对象这就是哈希值起的作用;
- GC 分代年龄: 对象首先是在Eden中创建的,在经过多次GC后,如果没有被进行回收,就会在 survivor 中来回移动,其对应的年龄计数器会发生变化,达到闽值后会进入养老区,
- 锁状态标志: 在同步中判断该对象是否是锁:
- 线程持有的锁;
- 线程偏向ID;
- 偏向时间戳。
实例数据
它是对象真正存储的有效信息,包括程序代码中定义的各种字段类型,当然也包含从父类继承下来的字段。注意这里有一些规则: 相同宽度的字段总是被分配在一起,父类中定义的变量会出现在子类之前,因为父类的加载是优先于子类加载的。
对齐填充
没有特殊含义,仅仅起到占位符的作用。