1. 对象的实例化
1.1 对象的创建
当程序中定义了一个类之后,我们的目的是为了使用它的对象,并通过对象使用类中所提供的方法,因此,在类创建完毕之后,下一步就需要实例化类对象,即创建类对象。用于对象创建的方式主要有如下几种:
- 通过new关键创建:最直接的方式是用户直接通过new创建;更好的方式是使用满足设计模式原则的单例模式或是工厂模式等进行创建
- 通过反射机制来创建:这里主要引用到通过发射机制得到的类的Class对象,以及类的构造方法来创建对象
- 深拷贝:克隆和反序列化都可以归为深拷贝,不同之处在于两者的实现机制不同
- 第三方库
2. 创建的步骤
对象实例化的过程主要包含如下步骤:
- 加载类元信息
- 为对象分配内存
- 处理并发问题
- 属性的默认初始化
- 设置对象头的信息
- 属性的显式初始化、代码块中初始化、构造器中初始化
2.1 判断对象对应的类是否加载、链接、初始化
虚拟机遇到一条new指令,首先去检查这个指令的参数是否存在MetaSpace的常量池中定位到一个类的符号引用,并且检查这个符号引用对应的类是否已经被加载、解析和初始化,即判断类元信息是否存在。如果没有,则通过双亲委派机制使用当前类加载器以ClassLoader+包名——类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象。
2.2 为对象分配内存
如果前一步可以通过类加载器在字节码文件中找到对应的类信息,并生成了对应的Class类对象。接着就需要为对象分配堆内存空间,分配的方式取决于内存空间是否规整:
- 如果规整:指针碰撞法。如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Pointer)来为对象分配内存。即所有已用内存在一边,空闲的内存空间在另一边,中间的指针作为两者的分界点的指示器。分配内存就仅仅是把指针向空闲那边移动一段与对象大小相等的距离。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有整理(compact)过程的收集器时,使用指针碰撞
- 如果不规整:空闲列表法。如果内存不规整,已使用的内存和未使用的内存相关交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。即虚拟机维护了一个列表,记录哪些内存块时可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式称为空闲列表(Free List)
当然,选择哪种分配方法由Java堆是否规整决定,而堆是否规整又由所采用的的垃圾收集器是否带有压缩整理功能决定。所以,在使用的过程中应该根据虚拟机具体的情况具体选择。
2.3 处理并发安全问题
通常处理方式有如下两种:
- 采用CAS失败重试、区域加锁保证更新的原子性
- 每个线程预先分配一块TLAB,通过 -XX:+UseTLAB参数设置,HotSpot默认开启
2.4 初始化分配到的空间
这里涉及的操作是为所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。Java中的基本数据类型设置对应的初始值,自定义类设置为null。
2.5 设置对象的对象头
将对象的所属类(类的元数据信息)、对象的HashCode和对象的GC信息、锁信息存储到对象的对象头中,这个过程的具体实现由JVM决定。
2.6 执行init()
进行初始化
在Java程序的视角看来, 初始化才正式开始,主要操作有:初始化成员变量、执行实例化代码块、 调用类的构造方法, 并把堆内对象的首地址赋值给引用变量。因此一般来说 (由字节码中是否跟随有invokespecial指令所决定), new指令之后会接着就是执行方法, 把对象按照程序的意愿进行初始化, 这样一个真正可用的对象才算完全创建出来;
3. 对象的内存布局
假设此时程序如下所示:
class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer(){
acct = new Account();
}
}
class Account{}
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
经过实例化过程得到的对象在栈、堆和方法区中的布局如下所示:
main()
对应的栈帧中包含局部变量表、动态链接等应有的内容,其中 局部变量表大小为2,分别存放了变量args和类变量cust。通过反编译的结果也正验证:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class ObjectTest/Customer
3: dup
4: invokespecial #3 // Method ObjectTest/Customer."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 22: 0
line 23: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 cust LObjectTest/Customer;
局部变量表中的cust指向了堆中创建的Customer实例。其中堆中的Customer实例中包含了运行时元数据和类型指针:
- 运行时元数据:包括实例的hashcode、GC分代年龄、锁状态标志……
- 类型指针:Customer类中的Account成员变量对应的在方法区中类元信息的地址
另外,实例数据也满足如下的一些规则:
- 相同跨度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields参数为true:子类的窄变量可能插入到父类变量的空隙
- ……
4. 对象的访问定位
程序创建类对象的最终目的是为了使用它,通过栈中保存的对象的地址就可以访问到对象。对象的访问方式有:
- 句柄访问
- 直接指针
这部分内容在Java虚拟机内存模型中有介绍,这里就不再赘述。