Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程

Step2:分配内存

当一个对象的类已经被加载后,会依据第一阶段分析的方式去计算出该对象所需的内存空间大小,计算出大小后会开始对象分配过程,而内存分配就是指在内存中划出一块与对象大小相等的区域出来,然后将对象放进去的过程。但需要额外注意的是:Java的对象并不是直接一开始就尝试在堆上进行分配的,分配过程如下:

image.png|550

1、栈上分配

栈上分配是属于C2编译器的激进优化,建立在逃逸分析的基础上,使用标量替换拆解聚合量,以基本量代替对象,然后最终做到将对象拆散分配在虚拟机栈的局部变量表中,从而减少对象实例的产生,减少堆内存的使用以及GC次数

逃逸分析:逃逸分析是建立在方法为单位之上的,如果一个成员在方法体中产生,但是直至方法结束也没有走出方法体的作用域,那么该成员就可以被理解为未逃逸。反之,如果一个成员在方法最后被return出去了或在方法体的逻辑中被赋值给了外部成员,那么则代表着该成员逃逸了
标量替换:建立在逃逸分析的基础上使用基本量标量代替对象这种聚合量,标量泛指不可再拆解的数据,八大基本数据类型就是典型的标量。

决定一个对象能否在栈上分配的因素(两个都必须满足):

  • ①对象能够通过标量替换分解成一个个标量
  • ②对象在栈帧级作用域不可逃逸栈帧逃逸:当前方法内定义了一个局部变量逃出了当前方法/栈帧

如果对象被分配在栈上,那么该对象就无需GC机制回收它,该对象会随着方法栈帧的销毁随之自动回收。但如果一个对象大小超过了栈可用空间(栈总大小-已使用空间),那么此时就不会尝试将对象进行栈上分配。

栈上分配因为是建立在逃逸分析之上的,所以能够被栈上分配的对象绝对是只在栈帧内有用的,也就代表栈上分配的对象不会有GC年龄,随着栈帧的入栈出栈动作而创建销毁。

2、TLAB分配

TLAB全称叫做Thread Local Allocation Buffer是指JVM在Eden区为每条线程划分的一块私有缓冲内存。在上篇对于JVM内存区域分析的文章中曾分析到:大部分的Java对象是会被分配在堆上的,但也说到过堆是线程共享的,那么此时就会出现一个问题:当JVM运行时,如果出现两条线程选择了同一块内存区域分配对象时,不可避免的肯定会发生竞争,这样就导致了分配速度下降

在为对象分配内存时,往往会出现多条线程竞争同一块内存区域的“惨案”,虚拟机为了根治这个问题采取了为每条线程专门分配一块内存区域,这块区域就被称为TLAB区当一条线程尝试为一个对象分配内存时,如果开启了TLAB分配的情况下,那么会先尝试在TLAB区域进行分配。(程序启动时可以通过参数-XX:UseTLAB设置是否开启TLAB分配)。

而值得一提的是:TLAB并不是独立在堆空间之外的区域,而是JVM直接在Eden区为每条线程划分出来的。默认情况下,**TLAB区域的大小只占整个Eden区的1%**,不过也可以通过参数:-XX:TLABWasteTargetPercent设置TLAB区所占用Eden区的空间占比。

一般情况下,JVM会将TLAB作为内存分配的首选项(C2激进优化下的栈上分配除外),只有当TLAB区分配失败时才会开始尝试在堆上分配。

分配过程

当创建一个对象时,开启了激进优化的情况时,首先会尝试栈上分配,如果栈上分配失败,会进行TLAB分配,首先会比较对象所需空间大小和TLAB剩余可用空间大小,如果TLAB可以放下去,那么就直接将对象分配在TLAB区。如果TLAB区的可用空间分配不下该对象,则会先判断剩余空间是否大于规定的最大空间浪费大小,如果大于则直接在堆上进行分配,如果不大于则先使用空对象填充 _内存间隙_,然后将当前TLAB退回堆空间,重新根据期望值申请一个新的TLAB区,再次进行分配。如下:
image.png

在上面的TLAB分配过程分析中,提到了几个名词:最大空间浪费大小、内存间隙以及期望值,释义如下:
最大空间浪费:其意如名,是指JVM允许一个TLAB区最多剩余多少内存不使用,一般来说这个值是动态的
内存间隙:当前 TLAB不够分配时,如果剩余空间小于最大空间浪费限制,那么这个 TLAB区会被退回Eden区,然后重新申请一个新的TLAB,而这个TLAB被退回到Eden区之后,该TLAB的剩余空间就会成为孔隙。如果不管这些孔隙,由于TLAB仅线程内知道哪些被分配了,在GC扫描发生时,又需要做额外的检查,那么会影响GC扫描效率。所以TLAB回归Eden的时候,会将剩余可用的空间用一个dummy object(空对象) 填充满。如果填充已经确认会被回收的对象,也就是dummy object,GC会直接标记之后跳过这块内存,增加GC扫描效率。
期望值:期望值这个概念在JVM中是惯用的思想,无论是JIT还是GC等,都以期望值作为激进优化的基础,这个期望是根据JVM运行期间的“历史数据”计算得出的,也就是每次输入采样值,根据历史采样值得出最新的期望值。

注意:当TLAB退回给堆空间时,那原本里面存储的对象需要挪动到新的TLAB区域吗?

答案是不需要的,因为TLAB区本身使用的就是Eden区的内存划出来的,所以直接将间隙内存填充好空对象之后退回给堆空间即可,原本的对象不需要挪动到新分配的TLAB区中,照样是可以通过原本的引用指针访问之前位置中的对象的,唯一需要改变的就是将线程的TLAB区指向改成新申请的内存区域。

3、年老代分配

如果在TLAB区尝试分配失败后,对象会进行判定:是否满足年老代分配标准,如果满足了则直接在年老代空间中分配。可能有些小伙伴会疑惑:对象不是先尝试在新生代进行分配之后,再进入年老代分配吗?其实这是错误的概念,对象在初次分配时会先进行判定一次是否符合年老代分配标准,如果符合则直接进入年老代。

年老代分配条件

初次分配时,大对象直接进入年老代。
一般对象进入年老代的情况只有三种:大对象、长期存活对象以及动态年龄判断符合条件的对象,在JVM启动的时候你可以通过-XX:PretenureSizeThreshold参数指定大对象的阈值,如果对象在分配时超出这个大小,会直接进入年老代。

这样做的好处在于:可以避免一个大对象在两个survivor区域来回反复横跳。因为每次新生代GC时,都会将存活的对象从一个survivor区移动到另外一个survivor区,而一般来说,大对象绝对不属于朝生夕死的对象,所以就代表着:大对象被分配之后很大几率都会在两个survivor区来回移动,大对象的移动对于JVM来说是比较沉重的负担,内存分配、数据拷贝等都需要时间以及资源开销。同时因为大对象的迁移会存在耗时,所以也会导致GC时间变长。

所以对于大对象而言,直接进入年老代会比较合适,这也属于JVM的细节方面优化。

上述的这段是基于分代GC器而言的,实则不同的GC器对于大对象的判定标准也不一样,尤其是到了后面的不分代GC器,大对象则不会进入年老代,而是会有专门存储大对象的区域,如G1、ShenandoahGC中的Humongous区、ZGC中的Large区等。

4、新生代分配

如果栈上分配、TLAB分配、年老代分配都未成功,此时就会来到Eden区尝试新生代分配。而在新生代分配时,会存在两种分配方式:

  • ①指针碰撞:指针碰撞是Java在为对象分配堆内存时的一种内存分配方式,一般适用于Serial、ParNew不会产生内存碎片、堆内存完整的的垃圾收集器

  • 分配过程:堆中已用分配内存和为分配的空闲内存分别会处于不同的一侧,通过一个指针指向分界点区分,当JVM要为一个新的对象分配内存时,只需把指针往空闲的一端移动与对象大小相等的距离即可

  • ②空闲列表:与指针碰撞一样,空闲列表同样是Java在为新对象分配堆内存时的一种内存分配方式,一般适用于CMS等一些会产生内存碎片、堆内存不完整的垃圾收集器

  • 分配过程:堆中的已用内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的空闲内存块信息,当创建新对象需要分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并同步更新列表上的记录,当GC收集器发生GC时,也会将已回收的内存更新到内存列表。

上述的两种内存分配方式,指针碰撞的方式更适用于内存整齐的堆空间,而空闲列表则更适合内存不完整的堆空间,一般来说,JVM会根据当前程序采用的GC器来决定究竟采用何种分配方式。

在Eden区分配内存时,因为是共享区域,必然会存在多条线程同时操作的可能,所以为了避免出现线程安全问题,在Eden区分配内存时需要进行同步处理,在HotSpot VM中采用的是线程CAS+失败换位重试的方式保证原子性

Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式

Step5:执行构造方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,构造方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行构造方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来