java对象创建的过程
文章目录
- java对象创建的过程
- 1.检查是否类加载完成
- 2.给对象分配内存空间
- 3.给对象初始化零值
- 4.对象头设置
- 一、Java对象内存布局
- 二、Java对象访问定位
- 5.执行\字节码指令
1.检查是否类加载完成
JVM执行一条字节码new指令时,先去检查这个指令的参数是否能在Class常量池里定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载,解析和初始化过,如果没有,则执行对应的类加载检查过程(ClassLoader)。
2.给对象分配内存空间
类加载检查通过之后,JVM会为新生对象分配内存(为对象分配内存空间的大小在类加载完成后就能够确定),所以给对象分配内存空间就是把一块确定大小的内存块从Java Heap中划分出来。
- 内存划分的两种方式:
- 指针碰撞(Bump The Pointer)
假设Java Heap中的内存是绝对规整的,所有被使用过的内存放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器。那么分配内存的方式就是把这个指针向空闲的内存方式挪动一段对象大小相等的内存区域。 - 空闲列表(Free List)
假设Java Heap中的内存并不是规整的,被使用过的和空闲的内存相互交错放在一起,JVM维护了一个列表,记录Java Heap中哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
- *选择哪种分配方式由Java Heap是否规整决定,而Java Heap是否规整又由垃圾收集器是否带有**空间压缩整理(Compact)*的能力而决定
- 当使用Serial、ParNew等带有压缩整理过程的收集器时,系统采用的分配算法采用的是指针碰撞(简单高效)。
- 当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。
- 指针碰撞所存在的隐患
- 对象创建在内存中是频繁的,在并发情况下,指针碰撞是线程不安全的。
- 解决方案:
- CAS加上失败重试机制:对分配内存空间的动作进行同步处理(JVM采用的是CAS加上失败重试机制保证更新操作的原子性)
2. 本地线程分配缓冲-TLAB(Thread Local Allocation Buffer):每个线程在Java Heap中预先分配一小块内存。线程分配内存就在自己的本地缓冲区中进行。只有当本地缓冲区用完了,分配新的缓冲区时,才需要同步锁定。JVM是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
3.给对象初始化零值
JVM必须将分配到的内存空间(不包括对象头)都初始化为零值,如果使用了TLAB,则这一步会提前至TLAB分配时进行。
作用:保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用,使程序能访问到这些字段的数据类型所对应的零值。
4.对象头设置
JVM对对象进行必要的设置。例如:对象是哪个类的实例对象、如何才能找到类元数据信息、对象的哈希码(延后到Object::hashCode()调用才计算、对象的GC分代年龄等信息。这些信息存放在对象头中(Object Header),对象头会根据JVM运行状态的不同,如是否启用偏向锁,锁状态标志位,在不同时刻会有不同的设置。
一、Java对象内存布局
HotSpot虚拟机种,对象在堆内存中的存储布局可以划分为三个部分:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头(Object Header)-包含两类信息
- Mark Word:存储对象自身运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程的ID、偏向时间戳等。
Mark Word在32位操作系统中占4字节,64位操作系统占8字节。它是一个动态定义的数据结构。如上表格,在不同的锁状态下Mark Word中的内容含义都不相同 - KClass Point(类型指针):对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
- 实例数据(Instance Data)
- 对象存储的真正有效信息。程序代码中定义的各种类型的字段内容,包括父类继承和子类定义的字段都会被记录。
- HotSpot虚拟机默认的字段类型分配顺序:long/double、int、short、char、byte/boolean、oop(Ordinary Object Pointer)
- 相同宽度的字段总是会被分配到一起存放。先按照字段类型分配策略排序字段变量,然后再按照父类定义的变量会出现在子类之前。
- 可以通过+XX:CompactFields-true/false 来设置子类宽度较窄的变量允许插入父类变量的空隙之中,节省空间。
- 对齐填充(Padding)
- 作用:占位符,没特殊含义。HotSpot虚拟机中自动内存管理系统要求对象起始地址必须是8字节的整数倍。对象头已经被设计成8字节了,实例数据如果没有对齐的话,就需要一个对齐填充来补全。
二、Java对象访问定位
创建对象是为了后续访问对象。Java程序通过栈上的reference来操作堆上的具体数据。reference类型在《Java虚拟机规范》里面只规定它是一个指向对象的引用(抽象概念),怎么去访问定位到对象的具体位置,也是由具体的虚拟机去决定的。
主流的访问方式:
- 句柄
- 概念:Java Heap中可能会划分初一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据(类元信息)各自具体的地址信息。
- 优点:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时会移动对象)时只会改变句柄池中的实例数据指针,而reference并不需要修改。
- 缺点:间接通过句柄来定位对象具体位置,访问速度稍慢。
- 直接指针
- 概念:reference存储的直接就是对象地址,如果只是访问对象本身,就不需要一次间接访问的开销(如果访问的是类型数据,就会KClass指针定位到方法区中找到类元信息)。HotSpot虚拟机中主要是采用的直接指针的方式进行对象访问的。
- 优点:访问速度快,直接定位到对象的具体地址,节省了一次指针定位的开销。
- 缺点:相对于句柄方式,增加了维护reference的成本
5.执行<init>字节码指令
对象初始化零值后,但是该对象并没有真正被赋予字段的值,其他资源和状态信息还没有按照预设的构造好。只要当调用了构造函数,执行了Class中的<init>方法之后,这才是一个完整的对象。
如下图中
invokespecial最终调用了Object中的<init>:()v方法进行了对象真正意义上的初始化
Classfile /D:/ideaData/uc/uc-master/target/test-classes/com/test/Math.class
Last modified 2021-1-5; size 509 bytes
MD5 checksum 256db9e62fac24112898aa8a7501ccec
Compiled from "Math.java"
public class com.test.Math implements com.test.Subject
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #21 // learning math...
#4 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #24 // com/test/Math
#6 = Class #25 // java/lang/Object
#7 = Class #26 // com/test/Subject
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/test/Math;
#15 = Utf8 learn
#16 = Utf8 SourceFile
#17 = Utf8 Math.java
#18 = NameAndType #8:#9 // "<init>":()V
#19 = Class #27 // java/lang/System
#20 = NameAndType #28:#29 // out:Ljava/io/PrintStream;
#21 = Utf8 learning math...
#22 = Class #30 // java/io/PrintStream
#23 = NameAndType #31:#32 // println:(Ljava/lang/String;)V
#24 = Utf8 com/test/Math
#25 = Utf8 java/lang/Object
#26 = Utf8 com/test/Subject
#27 = Utf8 java/lang/System
#28 = Utf8 out
#29 = Utf8 Ljava/io/PrintStream;
#30 = Utf8 java/io/PrintStream
#31 = Utf8 println
#32 = Utf8 (Ljava/lang/String;)V
{
public com.test.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/test/Math;
}
SourceFile: "Math.java"