Java对于我们来说,它就是一门编程语言。Java程序在运行过程中无时无刻不在创建对象,在代码层面其实就是一个简单的new
的一个过程。但是底层实现逻辑并非如此。那么它究竟是如何进行创建对象的呢?接下来我们一起来一探究竟。
创建对象的过程
说之前先捋清一个大致的思路:创建对象的过程大致分为5步:
- Step1:类加载检查
- Step2:分配内存
- Step3:初始零值
- Step4:设置对象头
- Step5:执行init
Step1:类加载检查
当我们在Java程序中new一个对象的时候,在底层其实会有大概以下几步:
- 首先它会检查这个指令是否能在常量池中能否定位到一个类的符号引用
- 接着会检查这个符号引用代表的类是否已经被加载、解析、初始化。如果没有会进行一个
**类加载**
检查完类加载后就是分配内存了。(这里有人可能会问那该对象的具体内存是否确认呢?其实类加载完成后可以确认它所需要的内存了)
Step2:分配内存
现在我们已经知道了对象所占的内存,那么虚拟机是如何给对象在Java堆中分配内存的呢?
主要有两种分配方式:
- 指针碰撞
- 空闲列表
接下来我们详细说说这两种分配内存的方式:
指针碰撞
其实这种方式理解起来比较简单的,假设Java堆中的内存是绝对完整的,它会把使用过的内存和未使用过的内存划分开来。此时一边就是使用过的内存,一边就是未使用过的内存;那么他如何去给一个新的对象去划分空闲内存中的某块区域呢?其实很简单,就是借助一个指针(这里是不是呼应上了所谓的指针碰撞);当我们分配内存的时候就是把指针在空闲的内存区域中移动一个与要被创建对象大小相等的距离。这就是指针碰撞的方式
。
适用场景:
内存规整,不碎片化
空闲列表
这个其实理解起来更为简单。它无非就是指在Java堆中的内存并非是规整的(使用的内存和未使用过的内存没有划分开来),比较杂乱无章,此时虚拟机就得需要列表记录内存中哪些是已经使用的哪些是没有使用的,然后在给对象分配内存空间的时候在该列表中找一个足够的内存分给对象实例;并更新维护的列表。这种就叫做空闲列表(Free List)
。
适用场景:
堆内存碎片化
Tip:
说到分配内存的两种方式,就顺便提一句,
- 当使用的是
Serial``ParNew
等压缩整理过程的收集器的时候,系统采用的是指针碰撞的方式。 - 而当使用的是
CMS
这种基于清除的算法收集器,理论上就只能采用空闲列表。
分配内存如何保证线程安全的
上面我们将给新的对象分配内存的方式以及分配内存前的逻辑大致理完了。你是不是觉得很简单。其实就是这么简单。但是其实我们忽略了一个很重要的问题。我们回想起本篇文中第一段话:Java程序在运行过程中无时无刻不在创建对象,那么它是如何在并发环境下保证线程安全的呢?接下来我们简单的捋一下
其实保证线程安全还是两种方式:
- 将分配内存空间的动作进行同步处理(虚拟机底层的实现逻辑就是
CAS
+失败重试
)来保证分配内存空间的原子性。 - 还有一种就是将分配内存的动作按照线程划分在不同的空间中进行,也就是每个线程在Java堆中有有属于自己的一小块内存,这种方式叫做
本地线程分配缓冲 Thread Local Allocation Buffer TLAB
,当本地线程缓冲使用完了,再分配缓存区时才需要同步锁定。至于虚拟机是否使用TLAB 可通过参数-XX: +/-UseTLAB
来控制。
Step3:初始零值
当分配完内存后,虚拟机必须将分配到的内存空间(不包含对象头)都初始化为零值。如果使用了TLAB,那么这一步会在TLAB分配时进行。为什么虚拟机要有这番操作呢?
主要是为了保证对象的实例字段能够在Java代码中可以在不赋值的是否就可以访问直接使用,这样就能使Java程序访问这些字段所对应的数据类型的初始零值
Step4:设置对象头
接下来,Java虚拟机还需要对这些对象进行必要的设置,例如这些对象是哪些类的实例、以及如何才能找到类的元信息、对象的哈希码(实际对象的哈希码会延期到真正调用Object::hashCode()方法时才计算)、对象GC的分代年龄等信息,这些信息都会保存在对象头中(Object Header)之中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式
Step5:执行init
执行完上述操作后,对于Java虚拟机来说对象已经创建完了,但是对于Java视角来说,对象的创建才刚刚开始,还没有执行init
方法。所有的字段还都为零。对象中需要的其它资源和状态信息还没有按照原有的意图去构造好。所以一般来说,new指令
之后就会执行init
方法,按照Java程序员的意图去对对象做一个初始化,这样之后一个真正完整可用的对象才构造出来。