对象的创建过程
1.类加载检查
虚拟机遇到一条new 指令时,首先将去检查指令参数是否能在常量池中定位到这个类的符号引用,并检查这个符号引用代表的类是否已被加载过、解析和初始化过。没有,则必须先执行相应的类加载过程。
2.分配内存
- 对象所需的内存大小在类加载完成后便可知,为对象分配空间的任务等同于把一块确定大小的内存,从Java堆中划分出来。
- 分配方式有”指针碰撞“和”空闲列表“两种。选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
- 虚拟机采用以下两种方式来保证线程安全性:
- 指针碰撞(CAS+失败重试):CAS是乐观锁的一种实现方式。冲突失败就重试,直到成功为止。保证更新操作的原子性。
- 空闲列表(TLAB):为每个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存,或TLAB的内存已用尽时,再采用上述的CAS+重试 进行内存分配。
3.初始化零值
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。从而保证了对象的实例字段在Java代码中可不赋初始值就可直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
- 初始化零值完成后,虚拟机要对对象进行必要的设置。比如这个对象是哪个实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,存放在对象头中。另外根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5.初始化-执行init方法
- 以上工作完成后,从虚拟机角度来看,一个新的对象已经产生了。但从Java程序视角来说,对象创建才刚开始。<init>方法还未执行,所有的字段都还是为零。所以一般来说,执行new 指令后会接着执行<init>方法,把对象按照程序设计进行初始化,结束后才算完全创建完成。
对象的内存布局
1.打印对象的内存占用大小:
//open jdl 插件
ClassLayout.parseInstance(o).toPrintable();
2.在HotSpot 虚拟机中,对象在内存中布局分为3块区域:对象头、实例数据和对齐填充。
3.对象头包括2部分信息:
- 第一部分是用于存储对象自身的运行时数据(对象的哈希码、GC 分代年龄、锁状态标志等)叫Mark Word(标记字段)。
- 第二部分是类型指针(Klass Pointer)。即对象指向它的类元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。
- 64位虚拟机普通对象的对象头数据结构:
4.实例数据部分:是对象真正存储的有效信息,也是程序中所定义的各种类型的字段内容。
5.对齐填充部分:仅仅起占位作用。因为HotSpot虚拟机的自动内存管理系统要求,对象起始地址必须是8 字节(64位)的整数倍。
Object Header(128 bit) | ||||||||
Mark Word(64 bit) | Klass Word(64 bit) | |||||||
锁状态 | 56 bit | 4 bit | 1 bit | 2 bit | ||||
54 bit | 2 bit | 1 bit | 是否偏向锁 | 锁标志位 | 对象类型指针 | |||
无锁 | unused25 bit | hashCode 31 bit | unused 1 bit | 对象分代年龄 4 bit | 0 | 01 | 指向元数据对象的指针 | |
偏向锁 | 线程Id 54 bit | Epoch 2 bit | unused1 bit | 对象分代年龄 4 bit | 1 | 01 | 指向元数据对象的指针 | |
轻量级锁 | 指向栈中锁记录的指针 | 00 | 指向元数据对象的指针 | |||||
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | 指向元数据对象的指针 | |||||
GC标记 | 空 | 11 | 指向元数据对象的指针 |
对象的访问定位
- 创建对象的目的是使用对象。Java程序通过栈上的reference(引用)数据来操作堆上具体对象。
- 对象的访问方式由虚拟机实现而定。有使用句柄和直接指针两种。
- 使用句柄:Java堆中将会划分出一块内存来作为句柄池,栈上的reference中存储的就是对象的句柄地址。而句柄中包含了对象实例数据与类型数据各自的具体地址信息。最大好处是reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference 本身不需要修改。
- 直接指针:Java堆对象的布局中,就必须考虑如何放置访问类型数据的相关信息。而reference中存储的直接就是对象的地址。最大好处时速度快,节省了一次指针定位的时间开销。