1 对象实例化过程
1.1 对象创建的方法 - 了解
- new:最常见的方式、单例类中调用getInstance的静态类方法、XXXFactory的静态方法
- Class的newInstance方法:反射的方式,在JDK9里面被标记为过时的方法,因为只能调用空参构造器,并且权限必须为 public
- Constructor的newInstance(xxxx):反射的方式,可以调用空参或带参的构造器,权限没有要求
- clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone( )方法
- 使用反序列化:从文件中、从网络中获取一个对象的二进制流,序列化一般用于Socket的网络传输
- 第三方库 Objenesis
1.2 创建对象步骤 - 重要
字节码角度分析
public class ObjectTest {
public static void main(String[] args) {
Object obj = new Object();
}
}
main( ) 方法对应的字节码(后面细讲)
- 调用 new 指令后后,加载 Object 类
- 调用 Object 类的 init( ) 方法
0: new #2 // class java/lang/Object
3: dup # 复制操作
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: return
执行步骤角度(重要)
1)类加载检查
- 虚拟机遇到一条new指令,首先去检查这个指令的参数 能否在Metaspace(元空间)的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。如果该类没有加载,那么在双亲委派模式下对该类进行加载。
2)分配内存
- 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
- 内存分配方式有 指针碰撞和空闲列表两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
3)初始化零值
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4)设置对象头
- 将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
5)执行 init 方法
- 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了;
- 但从 Java 程序的视角来看,对象创建才刚开始,< init > 方法还没有执行,所有的字段都还为零 。所以一般来说,执行 new 指令之后会接着执行 < init > 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
内存分配的两种方式:指针碰撞与空闲列表 详解 (重要)
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法。
- “标记-清除”,内存不规整,维护一个空闲列表
- “标记-整理”(也称作"标记-压缩"),内存规整,使用指针碰撞分类内存
- 值得注意的是,复制算法内存也是规整的
内存分配中的并发问题(重要)
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
2 对象的内存布局 重要
对象在JVM中是怎么存储的
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
对象头信息里面有哪些东西
Hotspot 虚拟机的对象头包括两部分信息
- 第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等)
- 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
|-----------------------------------------------------------------------------------------------------------------|
| Object Header(64bits) |
|-----------------------------------------------------------------------------------------------------------------|
| Mark Word(32bits) | Klass Word(32bits) | State |
|-----------------------------------------------------------------------------------------------------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | OOP to metadata object | Nomal |
|-----------------------------------------------------------------------------------------------------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | OOP to metadata object | Biased |
|-----------------------------------------------------------------------------------------------------------------|
| ptr_to_lock_record:30 | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:30 | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
| | 11 | OOP to metadata object | Marked for GC |
|-----------------------------------------------------------------------------------------------------------------|
实例数据
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
对其填充
- 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
- 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
3 对象的访问定位(句柄 or 直接指针)
JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象
。对象的访问方式由虚拟机实现而定,目前主流的访问方式有 ① 使用句柄
和 ② 直接指针
两种
- 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,
reference 中存储的就是对象的句柄地址
,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
;
直接指针:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。
HotSpot采用的就是这种方式。
这两种对象访问方式各有优势。
- 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
- 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。