Back
详情
Java中new一个对象会经历哪些步骤

对象创建的一般过程

当虚拟机遇到一个字节码 new指令的时候,首先去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用。并且检查这个符号引用代表的类是否被虚拟机类加载器加载(即类是否已经被加载)

  1. 如果没有,必须先执行类加载的流程。

  2. 在类的检查通过过后,接下来虚拟机就会为新生成对象分配内存对象所需要的内存大小在类加载的时候决定。(对象内存分配后面将有独立的一小段讲解)。

  3. 内存分配完成后,虚拟机会将这块分配到的内存空间(不包括对象头)都初始化为零值,就是将这块内存空间进清理和初始化。

  4. 接下来虚拟机还需要进行对象进行初始化设置,比如元数据(对象是那个类的实例)、对象的哈希编码、对象的 GC 分代年龄、偏向锁状态等信息这些信息都用于存放到对象头(Object Header)中。

完成上述流程,其实已经完成了虚拟机中内存的创建,但是在 Java 执行 new创建对象的角度才刚刚开始,还需要调用构造方法初始化对象(可能还需要在此前后调用父类的构造方法、初始化块等)。

进行 Java 对象的初始化。即在 .class 的角度是调用 init()方法。如果构造方法中还有调用别的方法,那么别的方法也会被执行,当构造方法内的所有关联的方法都执行完毕后,才真正算是完成了 Java 对象的创建

对象内存分配流程

为对象分配空间的任务实质上是从 Jvm 的内存区域中,指定一块确定大小的内存块给 Java 对象。(默认是在堆上分配)。

基本概念

简单解释下上图中的一些概念:

逃逸分析

在堆中分配对象是唯一的选择吗?

《深入理解JVM虚拟机》:对象在Java堆中分配内存,这是一个普遍的常识了,但是有一种特殊情况,那就是如果经过逃逸分析后发现一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样能尽量减少将对象分配的堆中,减少OldGC或FullGC的次数,提高性能

什么是逃逸分析?
逃逸分析的基本行为就是分析对象动态作用域

  1. 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
  2. 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

快速判断逃逸:只要在方法内new的对象实体在外部被使用到了,则认为发生了逃逸,不管是不是静态,只要new的对象跑出了方法,注意关注的是对象实体,不是变量名

TLAB

TLAB,全称Thread Local Allocation Buffer, 即:线程本地分配缓存
这是一块每个线程私有的内存分配区域,它存在于Eden区,TLAB空间的内存非常小,仅占有整个Eden空间的1%。

作用

  • 加速对象的分配,由于对象一般分配在堆上,而堆是线程共享的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降。
  • 考虑到对象分配几乎是Java中最常用的操作,因此JVM使用了TLAB这样的线程专有区域来避免多线程冲突,提高对象分配的效率,称之为快速分配策略

局限性
不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。

  1. -XX:UseTLAB: 设置是否开启TLAB。
  2. -XX:TLABWasteTargetPercent: 设置TLAB空间所占用Eden空间的百分比大小。
  3. 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
  4. 因为TLAB很小,因此大对象一般无法分配到TLAB

下面我们重点讲下,对象是如何在堆中进行分配的

对象在堆上的分配策略

为对象分配空间的任务实质上是从 Jvm 的内存区域中,指定一块确定大小的内存块给 Java 对象。(默认是在堆上分配)。

对象在堆上分配有多种策略,下面一一进行介绍

指针碰撞

假设 Java 堆中内存是绝对规整的,所有使用过的内存都被放在一边,没有使用过的内存放在了另外一边。中间放着一个指针用来表示他们的分界点。

那所分配的内存仅仅是把那个指针向空闲的方向挪动一段与Java对象大小相等的距离,这种分配方式叫做“**指针碰撞”(Dump The Pointer)**。

空闲列表

但是如果 Java 堆中内存并不是规整的,已经使用的内存块,和空闲的内存块相互交错在一起,那就没有办法简单的进行指针碰撞了,虚拟机必须维护一个可用内存区域列表。记录哪些内存块是可以使用的。

在对象内存分配的时候就从列表中去找到一块足够大的内存空间划分给实例对象,并且更新列表上的记录。这种分配方式叫做**“空闲列表”(Free List)**。

策略选择

什么时候使用指针碰撞,什么时候才用空闲列表?

选择哪一种分配方式是由 Java 堆是否规整决定的,而 Java 堆是否规整又是由所采用的垃圾回收器是否有空间整理(Compact)的能力决定

当使用 Serial 、ParNew 等带指针压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单,又高效。
当采用 CMS 基于清除(Sweep)算法的收集器时,理论上只能采用复杂的空闲列表来分配内存。

并发内存分配

对象频繁分配的过程中,即使只修改一个指针所指向的位置,但是在并发的情况下也不是线程安全的,可能出现正在给 A 对象分配内存,指针还没有来得及修改,对象 B 又同时使用原来的指针进行内分配的情况。

解决这个问题有两种可选的方案:

  • 一种是对内存分配空间的动作进行同步处理-实际上虚拟机是采用CAS + 失败重试的方式来保证更新操作的原子性。
  • 另外一种就是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thred Local Allocation Buffer, TLAB), 那个线程要分配内存,就在那个线程分配内存,就在那个线程的本地缓冲中分配,只有本地缓冲用完了,分配新的缓冲区时才需要同步锁定,虚拟机是否使用 TLAB,可以通过 -XX:+/-UseTLAB参数设置