Java对象创建过程
基本概念
指针碰撞
一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是指针碰撞.
空闲列表
如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表
用何种方式要基于虚拟机堆内存是否规整,这又由采用的垃圾收集器是否带有压缩整理功能决定,所以类似Serial、ParNes等收集器时采用指针碰撞,而采用CMS这种基于Mark-Sweep算法的收集器时采用空闲列表。
TLAB(Thread Local Allocation Buffer,本地线程分配缓存)
TLAB 可以把内存分配的动作按照线程划分在不同的空间中,每个线程在Java堆中预先分配一小块内存,这就是TLAB
虚拟机通过-XX:UseTLAB设定它的,java层面与之对应的是ThreadLocal类的实现
分配过程
- 将所有内存分配的操作划分到多个不同线程中进行,预先从Eden区给每个线程分配一小块内存(默认Eden区的1%),称为TLAB
- 哪个线程需要分配内存,先在那一个线程的TLAB上进行分配
- 当线程的TLAB用完了或者TLAB剩余内存不足以存放对象时,向Eden区重新申请TLAB,再次尝试分配
- 如果放不下,则采用CAS的方式进行同步锁定,在Eden区尝试分配
- 如果还是放不下,在Eden区执行Young GC(minor GC),在Eden区尝试分配
- 如果还是放不下,则将对象分配至老年代
内存分配并发问题
对象创建在虚拟机中属于频繁操作,这就涉及到了并发操作(当给对象A分配内存并且还没有分配完毕时,给对象B分配相同的的内存区域)。
解决方案主要包括两种:CAS和TLAB
- CAS (compare and swap)是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。在这个内存分配上,CAS失败再来一次的时候要再去读取一遍old值。(实际上发现该位置有值了就不会再在该位置分配了,会去下一个空闲空间分配。)
- TLAB 可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存。当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
创建对象过程(new方法创建对象)
类加载检测
当虚拟机收到一条new指令时,首先将检查当前new的类是否在常量池被加载过(在常量池找到需要new的类的符号,检查其是否被初始化过)。如果没有,则执行相应的类加载过程;如果有则直接准备为新的对象分配内存。
为新生对象分配内存
对象所需内存大小在类加载完成后就已确定,分配内存的过程等同于将一块确定大小的内存从java堆划分出来。分配方式有指针碰撞和空闲列表两种,选择哪种分配方式由Java堆是否规整决定;Java堆是否规整又由选择的GC收集器是否带有压缩整理功能决定。
初始化缺省值
-
在内存分配完毕后,需要对分配后的内存空间初始化为缺省值(如果使用TLAB,则提前至分配TLAB时进行)。**这一步保证了Java对象无需赋值即可直接使用,程序能访问到这些字段的数据类型的缺省值。
-
抽象数据类型默认初始化为null,基本数据类型为0,布尔为false...
设置对象必要参数
- 对对象的对象头(Object Header)进行初始化,包括类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象头中,某些参数,比如是否启用偏向锁等,对象头会有不同的设置方式。
执行对象构造(init)方法
- 对于虚拟机来说,在执行init方法前,对象的创建已经结束了。但对于java程序来说,创建对象的过程还没有结束(在执行init方法之前,所有字段值均为零)。执行new指令后,会接着执行init方法,把对象按照程序员的想法进行初始化,才算完成了一个对象的创建。
Java对象分配
-
1)依据逃逸分析,判断是否能栈上分配?
- 如果可以,使用标量替换方式,把对象分配到
VM Stack
中。如果 线程销毁或方法调用结束后,自动销毁,不需要GC
回收器 介入。 - 否则,继续下一步。
- 如果可以,使用标量替换方式,把对象分配到
-
2)判断是否大对象?
- 如果是,直接分配到堆上
Old Generation
老年代上。如果对象变为垃圾后,由老年代GC
收集器(比如Parallel Old, CMS, G1
)回收。 - 否则,继续下一步。
- 如果是,直接分配到堆上
-
3)判断是否可以在
TLAB
中分配?- 如果是,在
TLAB
中分配堆上Eden
区。
- 否则,在
TLAB
外堆上的Eden
区分配。
- 如果是,在
-
- 动态年龄判断
- 大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代
- 否则,继续下一步
-
5)分配到Eden区
整理归纳对象创建整体过程
整体:
- 首次创建对象时,类中的静态方法/静态字段首次被访问时,Java 解释器必须先查找类路径,以定位 .class 文件;
- 然后载入 .class(这将创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在 Class 对象首次加载的时候进行一次;
- 当用 new 方法创建对象时,首先再堆上为对象分配足够的存储空间;
- 这块存储空间会被清零,这就自动地将对象中的所有基本类型数据都设置成了缺省值(对数字来说就是 0,对 boolean 和 str 也相同),而引用则被设置成了 null;
- 执行所有出现于字段定义处的初始化动作(非静态对象的初始化);
- 执行构造器。
init方法:
Java 在编译之后会在字节码文件中生成 init 方法,称之为实例构造器,该实例构造器会将语句块,变量初始化,调用父类的构造器等操作收敛到 init 方法中,收敛顺序为:
- 父类变量初始化
- 父类语句块
- 父类构造函数
- 子类变量初始化
- 子类语句块
- 子类构造函数
clinit 方法
Java 在编译之后会在字节码文件中生成 clinit 方法,称之为类构造器。类构造器同实例构造器一样,也会将静态语句块,静态变量初始化,收敛到 clinit 方法中,收敛顺序为:
- 父类静态变量初始化
- 父类静态语句块
- 子类静态变量初始化
- 子类静态语句块
若父类为接口,则不会调用父类的 clinit 方法。一个类可以没有 clinit 方法。
clinit 方法是在类加载过程中执行的,而 init 是在对象实例化执行的,所以 clinit 一定比 init 先执行。
特殊情形:
对象不一定是使用new进行实例化的,也可以是其他方式.
- 如果对象是通过 clone() 方法创建的,那么 JVM 把原来被克隆的对象的实例变量的值拷贝到新对象中
- 如果对象是通过 ObjectInputStream 类的 readObject() 方法创建的,那么 JVM 通过从输入流中读入的序列化数据来初始化那些非暂时性(non-transient)的实例变量;
参考:
jvm 优化篇-(5)-线程局部缓存TLAB 指针碰撞、Eden区分配 -XX:+UseTLAB -XX:+PrintTLAB -XX:TLABWasteTargetPercent