一、对象创建的六种方式
1. new关键字
直接通过new关键字调用类的构造器创建
2. Class的newInstance()方法
通过类对象的newInstance()方法利用反射创建对象,只能调用权限为public的空参构造器,若对应类没有此构造器则会抛出编译时异常ClassNotFoundException
//通过反射获取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();
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减少了很多反射的开销,因此速度更快,默认是这套。具体原理感兴趣可参考:
- Fastjson源码分析—ASM的作用和实现(1)
- 分析FastJSON为何那么快与字节码增强技术揭秘
6. 其他第三方库
还有一些其他第三方库可以用一些特殊的方式创建对象,例如Objenesis库。Spring中就集成了Objenesis库,在使用Cglib创建动态代理对象时就使用到了Objenesis。
参考:java中Objenesis库简单使用
二、对象创建的六个步骤
1. 判断对象对应的类是否已加载
以当前类加载器+类全名作为key在方法区中查找类是否加载,若没有则通过双亲委派模式尝试加载,若没有找到则抛出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方法初始化对象,包括对属性的显式初始化、执行代码块、执行构造器函数
java在编译后会在字节码文件中生成<init>方法,称为实例构造器,其中包括属性显式初始化/代码块/构造函数
三、对象的内存布局
1. 对象的内存布局
堆中一个对象的空间中除了存储对象的属性外,还有对象头(markword、类型指针)和对齐填充。对象的内存布局如下:
2. 案例
public class CustomerTest {
public static void main(string[] args) {
Customer cust = new customer();
}
}
如上程序在创建Customer对象后jvm整体状态如下:
四、对象的访问定位方式
JVM是如何通过栈帧中的对象引用访问到对应的对象实例的呢?有两种如下方式
1. 句柄访问
通过在Java堆设置一个句柄池实现间接访问
优点:垃圾回收整理对象改变了对象位置后只需要修改句柄池中的指针,局部变量表中的引用指针无需改变
缺点:句柄池要占用一块存储空间,并且间接访问的方式效率较低
2. 使用直接指针(Hotspot采用)
使用直接指针直接访问
优点:访问速度比句柄访问更快
缺点:对象移动后需要同时修改栈中所有指向了这个对象的指针,要麻烦一些