JVM之对象实例化

182 阅读5分钟

本文我们将介绍一下在java程序中创建对象的方式&创建对象的步骤

前置知识:需要了解一下类加载子系统中各个阶段的工作。

1.创建对象的方式

Java程序中,创建对象主要有以下几种方式:

  1. new关键字创建;

    • 常规:通过new关键字调用类构造器创建对象;

    • 变形1:调用对象的getXXXInstance方法(单例模式);

    • 变形2:XXXBuilder/XXXFactory的静态方法。

  2. 反射方式

    Class的 newInstance 方法【只能调用空参构造器,要求被调用的构造函数是可见[public]的】

    Constructor 的 newInstance 方法【可以调用空参、带参的构造器,权限无要求】

  3. 使用类的clone方法【不调用任何构造器,当前类需要实现Cloneable接口,实现clone方法】;

  4. 使用反序列化【从文件中、网络中获取一个对象的二进制流】;

    提一嘴:《Java虚拟机规范》并没有指明二进制字节流必须从某个Class文件中获取。

  5. 第三方库Objenesis

    Spring源码中有用到,读者可自行查阅相关资料。

2.创建对象的步骤

public class CreateObj {
    public static void main(String[] args) {
        Object obj = new Object();
    }
}

摘取字节码指令

Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: return

创建对象过程分为以下几个步骤:

  1. 判断对象对应的类是否已经加载、链接和初始化

    虚拟机遇到一条new指令,首先要去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析、和初始化(即判断类元信息是否存在)

    • 如果没有,那么在双亲委派机制模式下,使用当前类加载器以及ClassLoader+包名+类名为Key查找对应的.class文件,如果没有找到文件,就抛出 ClassNotFoundException异常。
    • 如何找到,则进行类加载,并生成对应的Class类对象

    注意:加载阶段将每个class常量池中的符号引用值转存到运行时常量池中。类在解析之后将符号引用替换成直接引用,《java虚拟机规范》并未规定解析阶段发生的具体时间,只要求在执行一些字节码指令(包含new)之前,先对它们所使用的符号引用进行解析

  2. 为对象分配内存

    计算对象占用空间大小,然后在堆中划分一块内存存放对象。内存分配存在以下两种情况:

    • 内存规整时,使用指针碰撞。所有用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针往空闲那一边挪动一段与对象大小相等的距离即可。
    • 内存不规整时,虚拟机需要维护一个列表,使用空闲列表分配。这种情况下,已使用的内存和未使用的内存相互交错,列表中需要记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新空闲列表。

    选择哪种分配方式由Java堆内存是否规整决定,而Java堆内存是否规整又取决于垃圾收集器是否带有压缩的功能。

  3. 处理并发安全问题

    • 采用CAS自旋、区域加锁保证更新的原子性
    • 每个线程预先分配一块TLAB - 通过-XX:+/-UseTLAB参数来设定
  4. 初始化分配到的空间

    默认值初始化(不包括对象头)。

  5. 设置对象的对象头

    将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存放在对象的对象头中。

  6. 执行<init>方法进行初始化

    • 在Java程序员的视角看来,初始化才正式开始。初始化成员变量、执行实例化代码块,调用类的构造方法,并把堆内对象首地址赋值给引用变量
    • 因此一般来说(由字节码中是否跟随着有 invokespecial指令决定),new指令之后会执行方法,把对象按照程序员的意愿进行初始化,这样一个真正的可用的对象才算完全的创建出来
    • 显示初始化 、代码块中初始化、构造器中初始化;

3.dup指令作用

一个对象创建的套路是这样的:newdupinvokespecial。为什么创建一个对象需要三条指令呢?

首先,我们需要清楚类的构造器函数是以<init>函数名出现的,被称为实例的初始化方法。调用 new 指令时,只是创建了一个类的实例,但是还没有调用构造器函数,使用 invokespecial 调用了 <init> 后才真正调用了构造器函数,正是因为需要调用这个函数才导致中间必须要有一个 dup 指令,不然调用完<init>函数以后,操作数栈为空,就再也找不回刚刚创建的对象了。

image-20230126102117002.png

说明:创建一个对象不是一个原子操作。