一. 基础
- 先了解几个面试题:
- JUC 层面,说以下AQS大致流程
- CAS自旋锁,获取不到锁时会一直自旋吗? CAS 与 synchronized区别在哪,为什么CAS好,
- CAS 自旋时能保证当前自旋的线程一直占用cpu吗,如果cpu放弃了当前这个线程,是不是还要带来线程再次抢占cpu的开销
- synchronized 底层如何实现,在同步时有没有用到cas,具体在哪里用到了
- 对象头中存储了那些信息,长度是多少
- Object obj = new Object(): 说一下new一个对象,在jdk8下默认会占用多少内存空间
- JVM中对象布局: 在HotSpot虚拟机(也就是我们常说的java虚拟机)中,对象在堆内存中,存储布局可划分为三个部分:对象头Header, 实例数据Instance Data, 和对齐填充Padding
- 注意点在JVM虚拟机中可以将对象分为两种:JavaObject与ArrayObject,两种类型的对象唯一区别是ArrayObject对象头中多了一个Length
对象头
- 对象头由: 对象标记MarkWord, 类型指针Class Pointer(又叫类元信息klass pointer) 两部分组成
- 其中类型指针(类元信息)中存储的是指向该对象类元数据的首地址
- MarkWord对象标记中存储了对象在runtime时用的信息: 哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳,转发指针等等
- hashCode: 可以理解为对象的身份id,例如我们去获取一个对象的hashCode时,实际获取的就是该值
- GC 标记:
- GC 分代年龄(分代次数): 例如垃圾回收时,什么时候在新生代,什么时候放入老年代,等等回收判断次数就是根据次数进行记录,获取判断的
- 在 64位系统中 MarkWord 与 类型指针 各占8个字节,一共16个字节,在64位系统中对象头MarkWord 占64bit位,存储结构大致如下
- 其中 “25bit unused” 前25位不使用
- “31bit” 用来存储hashCode编码
- 用 “4bit” 存储分代年龄, 例如存储最大分代年龄为"1111" 二进制转10进制,我们了解到最大分代年龄为15次
- 一位"1bit"用来标记是否是偏向锁
- “2bit”: 2位存储锁标志位
- 类型指针: 对象指向它的类元数据的指针,虚拟机通过这个指针来确定当前对象时哪个类的实例,对象实例在堆空间中,类元信息在方法区中
- 总结: 对象头由: 对象标记MarkWord 与 类型指针Class Pointer(又叫类元信息) 两部分组成, MarkWord对象标记中存储了对象: 哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳、转发指针等等信息,在64位系统中对象头MarkWord 占64bit位,其中31位用来存储hashCode, 4位用来存储分代年龄,通过此处我们也可以在这里看出最大年龄"1111"转10进制为15次,锁标志等信息,类型指针: 对象指向它的类元数据的指针,虚拟机通过这个指针来确定当前对象时哪个类的实例,对象实例在堆空间中,类元信息在方法区中,在64位系统中MarkWord 与类型指针分别占用8个字节大小(类型指针不压缩的情况下是8个字节,默认会开启压缩4个字节)
- 了解对象头源码可以查看openJDK8下的markOop.hpp,会发现内部有几个属性
实例数据
- 存放类的属性数据信息,包括父类的属性信息等,如果是ArrayObject数组类型对象,实例数据中还包括数组长度,这部分会占用4个字节对齐
- 以下方自定义类MyObject 为例,内部有int类型与char类型两个属性,其中int类型为32位(32/8=4)占4个字节,char占1个字节,加上前面对象头16个字节,此时计算下来就是21个字节,在通过对齐填充扩充为8个字节的倍数+3个字节,那么该对象最少占用24个字节
class MyObject {
int i = 1;
char c = 'a';
}
对齐填充
- 对齐填充,为的是保证对象大小是8个字节的倍数,假设当前创建对象对象头+实例数据大约占14个字节,为了方便jvm计算,将其扩充为8个字节的倍数,对齐填充就设置为两个字节,这样(14+2=16) 保证了是8个字节的倍数,假设创建的对象刚好是8个字节的倍数,那么对齐填充将不会补充
jol-core工具证明对象布局与压缩指针
- 项目中引入一个依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
- 示例代码(通过运行main方法,创建Object对象,通过jol-core下的ClassLayout查看Object对象细节)
import org.openjdk.jol.info.ClassLayout;
public class ObjectHeader {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
- 打印输出如下图: 其中:
- 注意点,如果要看后面VALUE中的数字编码需要从后向前看
- 在第二个红框处,说明当前创建的对象占12个字节,补齐填充加了4个字节
- 在第三个红框处说明总共占16个字节
- 由上面打印出来的数据我们看到: Object实际占有了12个字节,由于不是8字节的倍数,通过对齐填充补了4个最终对象占16个字节,那么我们前面学对象头的时候,说了在 64位系统中对象头中 MarkWord 与 类型指针各占8个字节,为什么我们打印出来的类型指针只有4个字节
- 在了解一个命令(查看JVM启动时使用参数): “java -XX:+PrintCommandLineFlags -version” ,发现输出JVM默认使用 " -XX:+UseCompressedClassPointers" 表示压缩CalssPointers, 所以上面我们输查看对象头中类型指针只占4个字节
- 当我们执行" -XX:-UseCompressedClassPointers" 关闭压缩后再查看对象字节数,就发现此时类型指针变为了8个字节