一、对象的实例化
1、创建对象的方法
- new -- 最常用的方法
- Class的newinstance() -- 反射的方式,只能调用空参的构造器,权限必须是public。jdk1.8之前有效
- Constructor的newInstance() -- 反射的方法,可以调用空参、带参的构造器,权限没有要求。jdk1.8之后有效
- 使用clone() -- 当前类实现Cloneable接口
- 使用反序列化
- 第三方库Objenesis
2、对象创建的步骤
1、判断对象对应的类是否加载、链接、初始化
虚拟机遇到一条 new 指令的时候,首先去方法去中查找这个类是否被加载过。如果没有,在双亲委派机制模式下,使用当前类加载器 ClassLoader + 包名 + 类名 作为 key 查找对应的 .class 文件,没有找到抛出 ClassNotFoundException 异常,找到的话则进行类加载,生成对应的 Class 类对象
2、为对象分配内存
计算对象占用空间大小,接着在堆中划分一块内存给新对象
3、处理分配对象内存时的并发问题
堆是线程共享的,所以在多线程情况下分配对象可能会有并发问题。一般会优先分配在每个线程的 TLAB 中,分配失败的话,会采用 CAS 失败重试、加锁来保证分配的原子性
4、为对象属性默认初始化
也就是给对象属性赋零值
5、设置对象的对象头
将对象的所属类(类的元数据信息)、对象的 HashCode 、GC信息、锁信息等数据存储在对象头中。也就是将对象指向方法区中的类信息,将两者关联起来
6、执行 init 方法进行初始化
也就是显示初始化。如果站在程序的角度来看的话,这一步才算初始化的正式开始,在这一步会初始化成员变量,执行实例化代码块,调用类的构造方法等来初始化对象
二、对象的内存布局
大家都知道,如果 new 一个对象,就是在堆空间中开辟了一块内存,但是这块内存的布局是什么样的呢?主要包括 3 部分
- 对象头
- 示例数据
- 对齐填充
1、对象头
主要包含两部分
(1)运行时元数据
- 哈希值(HashCode):对象的地址值
- GC分代的年龄:也就是对象的年龄,在 GC 的时候会使用
- 锁状态标志
- 线程持有的锁
- 偏向线程 ID
- 偏向时间戳
(2)类型指针
指向方法区中这个对象所属的类型。也就是说每个对象都知道它的类是谁,就是由这个类型指针确定的。
注:如果创建的是数组(数组也是一个对象,也放在方法区中),还需要记录数组的长度。
2、示例数据
主要就是代码中定义的各种类型的字段(不光是自己类中的,还包括从父类继承下来的)
3、对齐填充
不是必须的,没什么特别含义,仅仅起到占位符的作用
4、画图分析
先定义如下代码:
public class Customer {
int id = 1;
String name;
Account acc;
{
name = "张三";
}
public Customer() {
acc = new Account();
}
}
class Account {
}
/**
* 主方法
*/
public class NewTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
在如上代码中对应的内存图如下:
- 主线程中 main() 方法入栈。main() 方法栈帧的局部变量表中有两个变量,一个是方法参数 args,一个是自己创建的Customer类的对象 cust。
- 我们在 new Customer() 后,会在堆空间创建一个 Customer 实例。局部变量表中 cust 指向堆空间中创建的对象实例。
- 在 Customer类的对象实例中,有对象的信息。包括对象头信息、实例数据信息。在实例数据信息中,有父类的实例数据,还有自己定义的实例数据,其中 id 值为 1,name 的值指向堆空间中的字符串常量中的 "匿名客户",acct 指向 Account 类的实例。
- Customer 对象实例中的类型指向方法区中 Customer 类的 Class 信息。Account 对象实例中的类型指向方法区中 Account类的 Class 信息。
通过以上关系图,就很清晰的知道了栈、堆、方法区之间的联系了
三、对象的访问定位
HotSpot 采用的就是指针的方法。栈中对象引用直接指向堆空间中的对象实例,堆空间中对象实例中的类型指针又指向方法区中该类的类元信息。具体如下如所示: