对象创建流程
JVM 创建一个对象的总体流程如下图所示:
下面分别对这几个部分详细介绍:
一、常量池检查和类加载
在 Java 程序运行时,当虚拟机首次遇到 new 指令尝试创建类的实例时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,接着使用运行时常量池中的符号引用来检查所需的类是否已经被加载、链接和初始化。如果发现类尚未加载,JVM 将启动类加载过程。
- 注意这个“所需类”首先指的是 new 指令直接尝试创建实例的那个主类,但在整个类加载和初始化过程中,所有相关的依赖类(包括父类、接口、字段类型等)也都将被加载和初始化,以保证类的完整性和程序的正确运行。
类加载过程中class常量池与运行时常量池的互动:
- 编译期间的常量池(class 常量池):
- 编译期间,Java 编译器会将类的所有符号引用(如类、方法、字段的引用等)保存在类文件的常量池(Class 常量池)中,这是一个编译时构建的数据结构。此外,类文件的常量池还包括各种字面量和其他常量信息。
- 运行时常量池的加载:
- 当类被 JVM 加载时,类文件中的常量池内容(包括所有的符号引用和字面量)被转移到 JVM 的运行时常量池中。运行时常量池是方法区的一部分,用于存储类在运行期间需要的各种常量以及引用信息。
- 类实例化过程:
- 在 Java 程序运行时,当 JVM 遇到一个 new 指令尝试创建类的实例时,它首先在运行时常量池中查找这个类的符号引用。
- JVM 利用这些符号引用来检查所引用的类是否已经被加载、链接(验证、准备、解析)和初始化。
- 如果类未被加载,JVM 会触发类加载器加载这个类,这包括读取类的字节码、验证字节码的格式正确性、为静态字段分配内存并设置默认值、解析符号引用到具体引用以及执行静态初始化器(如静态字段赋值和静态代码块)。
二、分配内存空间
确认类已加载后,JVM 开始为对象在堆中分配内存空间。
内存分配方式
不同的 GC 垃圾收集器会有不同的内存分配策略:
指针碰撞 | 空闲列表 | |
---|---|---|
使用场景 | 堆内存规整 | 堆内存不规整 |
原理 | 分界指针 | 空闲内存块列表 |
GC 收集器 | Serial、ParNew | CMS |
创建对象是很频繁的任务,在内存分配过程中会存在线程并发问题,为保证线程安全,通常有下面两种方案:
- CAS乐观锁失败重试
- TLAB(ThreadLocal allocated buffer):为每一个线程在 Eden 内分配内存(称为 TLAB),线程独有的 TLAB 放不下的时候再尝试 CAS 失败重试。
对象会首先被分配到堆的哪个分代呢?(新生代?老年代?)
堆内存在 JDK1.8 之后被分为年轻代和老年代,分别在什么情况下会分配到哪个分区呢?
- 默认情况下 Eden 区占年轻代的 0.8,S0、S1 分别占 0.1
- 老年代空间是年轻代的二倍
新对象大多进入年轻代(Eden),那么对象在什么情况下才会进入老年代呢?对象进入老年代的四种情况
- 年龄太大,经历 MinorGC 15 次;(-XX:MaxTenuringThreshold
- 动态年龄判断:MinorGC 后会动态判断老年代条件,符合条件的对象移入老年代
- 大对象直接进入老年代,节省复制成本(内存担保机制);(-XX:PretenureSizeThreshold)
- MinorGC 后存活对象太多,无法放入 Survivor
详细的分配空间流程图:
内存担保机制
内存担保机制就是为了保证老年代持续有能力为新生代对象的容纳提供担保。(为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间
三、初始化零值
在第二步内存分配完毕后,将分配的内存空间初始化为零值(不包括对象头)
四、设置对象头
将必要的对象信息存放到对象头中,比如:
- 该对象是哪个类的实例
- 如何找到类的元数据信息
- 对象的哈希码
- GC 分代年龄等信息
五、执行 init 方法
上面四步执行结束后,在虚拟机的视角一个对象已经创建完成了
最后,会调用类的构造方法(即 init 方法),按照程序意图对对象进行初始化。到此为止才得到最终真正可用的对象。