1. 对象创建过程:
1. 类加载检查: 当 java 虚拟机遇到一条 new 指令时, 首先会去检查该指令的参数能否在常量池中定位到这个类的符号引用, 并且检查这个符号引用代表的类是否已被加载, 解析, 初始化过, 如果没有, 则必须先执行相应的类加载过程.
2. 分配内存: 类加载检查完成后, 虚拟机将为新对象分配内存空间, 且对象所需内存空间大小在其完成类加载检查后即可确定, 该过程其实就是在堆中划分一小部分的确定大小的空间, 用于存储对象信息. 其中分配方式有以下两种:
内存分配的并发问题:
在创建对象时存在线程安全问题, 虚拟机采用两种方式来保证创建对象的线程安全:
CAS 锁 + 失败重试: CAS 是乐观锁的一种实现形式, 虚拟机采用 CAS + 失败重试来保证更新操作的原子性.
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存, JVM 在给线程中的对象分配内存时, 首先在 TLAB 分配, 当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时, 再采用上述的 CAS 进行内存分配.
乐观锁: 乐观锁就是, 每次不加锁而是假设没有冲突而去完成某项操作, 如果因为冲突失败就重试, 直到成功为止
3. 初始化零值: 内存分配完毕后, 虚拟机将该对象分配得到的内存空间全部设置初始值零(不包含对象头部分), 该操作可以保证对象的实例字段在代码中即使不赋予初始值就可以直接使用. 程序能访问到这些字段的数据类型所对应的零值.
4. 设置对象头: 初始化零值完成后, 虚拟机将对象的一些必要信息存放在对象头中, 这些信息包括: 例如这个对象是哪个类的实例, 如何才能找到类的元数据信息, 对象的哈希吗, 对象的 GC 分代年龄等信息. 另外, 根据虚拟机当前运行状态的不同, 如是否启用偏向锁等, 对象头会有不同的设置方式.
5. 执行 Init 方法: 完成上述操作后, java 虚拟机即完成了一个对象的创建, 但是对于 java 程序而言, 对于该对象的一些定制的内容还未进行,<init > 方法中包含了程序员的定制需求和意愿, 执行完 init 方法后, 对象完成了初始化, 此时才是一个可用对象.
2. 对象的内存布局
虚拟机中, 对象在内存中的存储包括三个部分: 对象头, 实例数据和对齐填充.
Hotspot 虚拟机的对象头: 包括两部分信息, 第一部分用于存储对象自身的自身运行时数据(哈希吗, GC 分代年龄, 锁状态标志等等), 另一部分是类型指针, 即对象指向它的类元数据的指针, 虚拟机通过这个指针来确定这个对象是那个类的实例.
实例数据部分: 是对象真正存储的有效信息, 也是在程序中所定义的各种类型的字段内容.
对齐填充部分: 不是必然存在的, 也没有什么特别的含义, 仅仅起占位作用. 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍, 换句话说就是对象的大小必须是 8 字节的整数倍. 而对象头部分正好是 8 字节的倍数(1 倍或 2 倍), 因此, 当对象实例数据部分没有对齐时, 就需要通过对齐填充来补全.
3. 对象的访问定位
Java 程序通过 java 栈上的 reference 数据来操作堆上的具体对象. 对象的访问方式有虚拟机实现而定, 目前主流的访问方式有1使用句柄和2直接指针两种:
1. 使用句柄: 使用句柄访问会在 java 堆中开辟一片区域作为句柄池, 栈中的 reference 中存储的就是对象的句柄地址, 而句柄中则包含了对象实例数据和类型数据的地址信息.
在 java 栈中的 reference 中, 包含了需要引用的对象的句柄地址, 然后通过句柄地址在句柄池中找到指向对象实例数据和对象类型数据的指针, 从而实现引用该对象.
优点: 引用中存储的是句柄地址, 在对象被移动 (垃圾回收时对象移动时很常见的) 时, 只需要改变句柄中的实例数据指针, 而引用中存储的句柄地址无需改变
缺点: 因为引用对象通过两次指针定位, 速度较直接指针访问慢
2. 直接指针: 使用直接指针访问时, reference 中存储的是对象地址:
优点: 节省了一次指针定位的操作, 速度较快.
来源: https://www.cnblogs.com/LearnAndGet/p/9767445.html