JVM笔记(7)—— Java对象创建的过程

一、对象创建的六种方式

在这里插入图片描述

1. new关键字

直接通过new关键字调用类的构造器创建

2. Class的newInstance()方法

通过类对象的newInstance()方法利用反射创建对象,只能调用权限为public的空参构造器,若对应类没有此构造器则会抛出异常InstantiationException

        //通过反射获取Test类的类对象
        Class cl1 = Class.forName("com.classLoader.Test");
        //调用Test类中权限为public的空参构造器创建对象
        //创建出的对象为object类型
        Object o1 = cl1.newInstance();

        Test o2 = (Test)o1;
        Class cl2 = o2.getClass();

        
        System.out.println("cl1 == cl2 " + (cl1 == cl2));//true 同一个类的类对象在jvm中只存在一个
        System.out.println("o1 == o2: " + (o1 == o2));//true 引用类型的强转只是返回一个新的引用变量,其指向的对象的存储地址没有改变

使用条件苛刻,jdk9后这个方法已经被标记为过时的了,需要反射创建对象时更推荐直接使用Constructor,Class的newInstance()方法中也是用的Constructor进行创建的,只不过在调用前加了限制。

3. Constructor的newInstance()方法

通过Constructor利用反射来创建对象,可调用类中任意构造器,包括私有的构造器

        Class clazz = Class.forName("com.createObject.Test");

        //获取空参构造器创建对象
        Constructor constructor1 = clazz.getDeclaredConstructor();
        //通过setAccessible(true)可以使用私有构造器方法
        constructor1.setAccessible(true)
        Object o1 = constructor1.newInstance();

        //获取参数类型为String的构造器创建对象
        Constructor constructor2 = clazz.getDeclaredConstructor(String.class);
        Object o2 = constructor2.newInstance("Hello");

4. 使用clone()方法

通过一个对象的clone()方法拷贝一个新的对象,对象所属类需要重写clone()方法,并实现Cloneable接口。若重写方法中直接调用父类Object的clone()方法,则为浅拷贝(即对于原对象中的引用类型变量,拷贝时直接复制引用,而非再复制一份对象)

		Test o1 = new Test();
        Test o2 = o1.clone();
        System.out.println("o1 == o2: " + (o1 == o2));//false 拷贝的对象为一个新的对象

在这里插入图片描述

5. 使用反序列化

将从文件或网络中获取一个对象的二进制流反序列化为对象。常用的反序列化方式有下面两种

(1)Java原生类ObjectInputStream

这是jdk自带的反序列化方式,序列化对象的类需要实现了Serializable或Externalizable接口

//将对象序列化并存储在文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\object.out"));
oos.writeObject(new User("xuliugen", "123456", "male"));

//从文件中读取对象的二进制流并反序列化为对象
ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.out"));
User user = (User) ois.readObject();
(2)JSON库

一些常见JSON库可以实现将一个对象序列化为一个json字符串,或者将json字符串反序列化为一个对象

Test o1 = new Test();
//fastjson 将对象序列化为json格式字符串
String jsonStr = JSON.toJSONString(o1);
//fastjson 反序列化字符串为对象
JSONObject jsonObject = JSON.parseObject(jsonStr);
Test o2 = JSON.toJavaObject(jsonObject, Test.class);

fastjson库中有两套序列化/反序列化框架。第一套是常规的,序列化的原理是将对象的属性名和属性值转换成JSON中的key和value,反序列化的原理是通过反射来set对应的属性值生成对象。第二套是基于ASM字节码框架,通过ASM减少了很多反射的开销,因此速度更快,默认是这套。具体原理感兴趣可参考:

  1. Fastjson源码分析—ASM的作用和实现(1)
  2. 分析FastJSON为何那么快与字节码增强技术揭秘

6. 其他第三方库

还有一些其他第三方库可以用一些特殊的方式创建对象,例如Objenesis库。Spring中就集成了Objenesis库,在使用Cglib创建动态代理对象时就使用到了Objenesis。
参考:java中Objenesis库简单使用

二、对象创建的六个步骤

在这里插入图片描述

1. 判断对象对应的类是否已加载

虚拟机遇到一条new指令,首先会去检查这个指令的参数能否在方法区的常量池中定位到一个类的符号引用,并检查这个符号引用所代表的类是否已经加载,如果没有,则以当前类加载器通过双亲委派模式尝试查找加载类,若没有找到则抛出ClassNotFoundException

2. 为对象分配内存

首先计算对象占用空间大小,然后在堆中分配一块内存给新对象。

如果JVM采用的垃圾收集器采用的是标记整理算法,即回收后会将剩余对象整理到连续的内存空间,使得堆内存规整,则JVM中给对象分配内存的方式是指针碰撞,即在已使用的空间后连续分配内存,以继续保持内存规整。

如果JVM采用的垃圾收集器采用的是标记清除算法,则堆内存是不规整的,已使用的内存和未使用的内存相互交错,那么虚拟机采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录哪些内存块是可用的,分配内存时在列表中找到一块足够大的空间分配给对象,再更新列表。

3. 处理并发安全问题

由于在堆中创建对象这个操作非常频繁,如果不对堆内存特殊处理,就可能出现并发安全问题。处理方式有两种:
(1)CAS+失败重试
CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
(2)每个线程预先分配一块TLAB
为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 内存已用尽时,再采用上述的 CAS 进行内存分配。

可通过-XX:+/-UseTLAB选项来配置jvm是否使用TLAB,默认是开启的

4. 初始化分配到的空间

给对象的所有属性(包括继承的父类的属性)设置默认值。例如int类型属性默认初始化为0

5. 设置对象的对象头

将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

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

执行init方法初始化对象,包括对属性的显式初始化、执行非静态代码块、执行构造器函数。如果是一个子类对象,还会执行父类属性显示初始化、执行父类的非静态代码块,在构造器函数的第一行调用父类的构造器进行初始化

<init>:java在编译后会在字节码文件中生成<init>方法,称为实例构造器,其中包括属性显式初始化/非静态代码块/构造函数,<init>方法每次构造对象时都会执行。
<clinit>:字节码文件中还有一个<clinit>方法,其中包括对类的静态变量的初始化、静态代码块,<clinit>方法只在类加载过程最后的初始化中执行一次。

三、对象的内存布局

1. 对象的内存布局

堆中一个对象的空间中除了存储对象的实例数据外,还有对象头(markword、类型指针)对齐填充。对象的内存布局如下:

在这里插入图片描述

在这里插入图片描述

markword内部结构:

在这里插入图片描述

参考:由浅入深,逐步了解 Java 并发编程中的 Synchronized

2. 案例

在这里插入图片描述

public class CustomerTest {
	public static void main(string[] args) {
		Customer cust = new customer();
	}
}

如上程序在创建Customer对象后jvm整体状态如下:

在这里插入图片描述

四、对象的访问定位方式

JVM是如何通过栈帧中的对象引用访问到对应的对象实例的呢?有两种如下方式

1. 句柄访问

通过在Java堆设置一个句柄池实现间接访问
优点:垃圾回收整理对象改变了对象位置后只需要修改句柄池中的指针,局部变量表中的引用指针无需改变
缺点:句柄池要占用一块存储空间,并且间接访问的方式效率较低
在这里插入图片描述

2. 使用直接指针(Hotspot采用)

使用直接指针直接访问
优点:访问速度比句柄访问更快
缺点:对象移动后需要同时修改栈中所有指向了这个对象的指针,要麻烦一些
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值