纸上得来终觉浅,绝知此事要躬行。
1 创建对象的方式
-
new
关键字Person person = new Person();
-
Class的newInstance()
Person person = Person().class.newInstance;
-
Constructor的newInstance(XXX)
// 包括public的和非public的,当然也包括private的 Constructor<?>[] declaredConstructors = Person.class.getDeclaredConstructors(); // 只返回public的~~~~~~(返回结果是上面的子集) Constructor<?>[] constructors = Person.class.getConstructors(); Constructor<?> noArgsConstructor = declaredConstructors[0]; Constructor<?> haveArgsConstructor = declaredConstructors[1]; noArgsConstructor.setAccessible(true); // 非public的构造必须设置true才能用于创建实例 Object person = noArgsConstructor.newInstance();
-
使用
clone()
Person person = new Person(); Object person1 = person.clone();
-
使用
反序列化
Person person = new Person("lcp", 18); byte[] bytes = SerializationUtils.serialize(person); // 字节数组:可以来自网络、可以来自文件(本处直接本地模拟) Object deserPerson = SerializationUtils.deserialize(bytes);
-
第三方库
Objenesis
2 创建对象的步骤
2.1 从字节码指令来看
Object o = new Object();对应的字节码指令为以下
//#2 是ava/lang/Object
//在执行本条指令时,先在方法区寻找是否加载了这个类
//如果没有加载,先去加载这个类至方法区,并对其完成链接和初始化
//如果加载了。在堆中为该对象分配空间,并进行默认初始化,即0/false/null
0: new #2 // class java/lang/Object
//复制该对象的引用入栈
3: dup
//#1 是java/lang/Object."<init>":()V
//即调用Object类的无参构造器,给对象显式初始化
4: invokespecial #1 // Method java/lang/Object."<init>":()V
// 将当前遍历从操作数栈存在到局部变量表下标为1的地方
7: astore_1
8: return
2.2 从执行步骤来看
-
判断对象对应的类是否加载、链接、初始化
虚拟机遇到一条new指令,首先先去检查这个指令的参数能否在MetaSpace的常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已经被加载、解析和初始化, 如果没有,那么在双亲委派机制下,使用当前类加载器以ClassLoader+包名+类名为key进行查找对应的.class文件,如果没有,则输出ClassNotFoundException异常, 如果找到则进行类加载,并生成对应的Class对象
-
为对象分配内存
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。 如果实例成员变量是引用类型,仅分配引用变量空间即可,即4个字节大小 如果内存整齐,使用指针碰撞法(Bump The Pointer)来为对象分配内存。 意思是所有用过的内存在一边,空闲的在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边移动一端与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式,一般使用带有compact(整理)过程的收集器时,使用指针碰撞。 如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。 意思是虚拟机维护了一个列表,记录了哪些那内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容,这种分配方式成为"空闲列表"(Free List) 选择哪种分配方式由堆是否整齐决定,而堆是否整齐又由所采用的垃圾收集器是否带有压缩整理功能决定
指针碰撞空闲列表 -
处理并发安全问题
在堆中为对象分配内存空间时,由于堆是线程共享的,需要考虑并发的问题,即A线程想在某地址为对象分配空间,B线程也想在该地址为对象分配空间,此时如果不处理并发,必然会导致某个线程为对象分配空间失败 为了解决该问题,有两种策略: 1。在堆中Eden区为每个线程分配一块TLAB。即堆中线程私有的缓冲区,每个线程创建对象,在私有的缓冲区内创建 2.使用CAS失败重试,区域加锁保证操作的原子性
-
默认初始化
当为该对象创建好空间后,进行默认初始化。即为所有成员默认赋值,int long赋值为0,double赋值为0.0 boolean赋值为false 引用类型赋值为null
-
设置对象的对象头
将对象所属的类信息,对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中,这个过程由JVM实现
-
显式初始化
执行构造方法,为对象中的成员赋值,并把对象的首地址赋值给引用变量
3 对象的内存布局
堆中的对象由 对象头+实例数据+对齐填充 组成
对象头
由类型指针
和运行时元数据
组成
- 类型指针:指向类元数据,即标明该对象所属类型
- 运行时元数据:哈希值 + GC分代年龄 + 锁状态标志 + 线程持有锁 + 偏向线程ID + 偏向时间戳
实例数据
存储对象真正的有效信息,包括父类及自己本身拥有的字段。父类中的成员出现在子类之前,相同宽度的成员被分配在一起
对齐填充
仅起到占位符的作用,不是必须的。
public class Customer {
int id = 1001;
String name;
Account acct;
{
name = "匿名用户";
}
public Customer() {
acct = new Account();
}
}
class Account {}
---------------------------------------
Customer cust = new Customer();
4 对象的访问定位
Person person = new Person();
person是一个引用变量,可以通过这个引用变量访问堆中真实的对象。
对象的访问方式有两种:句柄访问 和 直接指针
句柄访问:
堆中开辟出句柄池和实例池 引用变量指向的是堆中句柄池某句柄中的指针
想要访问对象需要先找到指向对象实例的句柄指针 通过该句柄指针才能访问到对象实例
优点:引用变量稳定存储句柄地址,对象被移动时(GC时移动对象很普遍)
只会改变句柄中指向对象的指针,引用变量本身不需要修改
缺点:开辟句柄池占用堆空间,经过两次指向才能访问到对象实例,效率低
直接指针:
引用变量的值就是对象在堆中的地址
优点:高效,直接,不占用堆空间
缺点:当发生GC时 对象被移动后 引用变量的值需要被修改
使用直接指针访问对象实例也是HotSpot VM访问对象的默认实现