JVM知识点-对象创建过程
注:更多文章,可以进入我的博客:筱白博客
对象的创建方式
① 通过new关键字
// Person zhangsan = new Person(id, height, weight)
Person zhangsan = new Person();
② 通过反射创建
//Class clz = Class.forName("Person类的全限定类名")
Class clz = Person.class;
Person zhangsan = clz.newInstance()
// 使用构造器创建
Constructor<Person> cons = clz.getConstructor()
// 也可以指定参数类型获取有参构造器
Person zhangsan1 = cons.newInstance()
③ 通过clone创建对象
当类实现了Cloneable接口时,可以使用clone()方法复制一个对象。需要留意是clone方法是浅拷贝
Person libo = new Person(name: "李博", age:12, ...)
Person Livonor = new Person(name: "Livorno", age:32, ...)
libo.setFather(Livonor)
Person zhangsi = libo.clone() // 此时,张四和张三的名字、父亲在内存中都引用了相同的对象
④ 反序列化创建
通过读取IO数据流创建,非本节重点
对象的创建过程
1、类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键字、反射、克隆、反序列化等操作。
2、分配内存空间
2.1 内存空间情况
- 对象所需要的空间在类加载后就可以确定,但是起始地址需要在分配时去内存中找到一块足够大的空间。
- 地址的分配有两种方式:指针碰撞和空闲列表。
2.2 内存分配方式
① 指针碰撞:
指针碰撞的方式是假设内存空间是规整的,被使用的和空闲的内存被分割成了两整块,通过一个指针记录分界点。在给对象分配内存的时候,将指针向空闲区域移动一段与对象空间大小相等的距离即可。
② 空闲列表:
如果内存不规整,那么就需要维护一张表,来记录内存中那些地址是空闲的。分配对象时,通过空闲列表去找到一块足够大的空闲内存分配给对象并更新空闲列表。
2.3 解决内存分配时并发情况的方法
多个线程创建的对象内存的冲突。举个例子,线程1和2同时要创建两个对象,指针是同一个。它们各自将指针加载到了工作内存中,然后去执行分配地址空间的指令。结果就导致,后分配的那一个,可能将先分配的那个对象的地址给覆盖了。
解决方案
① CAS
对分配内存的动作进行同步处理,即采用CAS加失败重试的方式,保证更新操作的原子性。
② 本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)
JVM虚拟机给每个线程在Java堆中预先分配一小块内存,每个线程创建对象都在自己对应的空间中完成,本地缓冲区用完了,分配新的缓冲区时才需要同步锁定。
3、初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,该工作可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4、设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。**这些信息存放在对象头中。**另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
对象头包含了两种信息:MarkWord和类型指针。
① MarkWord:存放对象本身的运行时状态数据(如HashCode, GC分代年龄、锁状态、是否偏向信息等)。
② 类型指针:是一个指向方法区中Class
信息的指针,虚拟机通过这个指针确定该对象属于哪个类的实例。
③ 数组长度:如果对象是一个数组对象时,那么在对象头中有一个保存数组长度的空间。
User[] user = new User[2];
5、执行< init >方法
将对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
对象的访问方式
① 句柄
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
其好处是,当对象被移动的时候(比如垃圾回收时,整理内存空间需要大量移动对象),不需要频繁的修改引用,只需要修改句柄中实例数据指针。
② 直接指针
如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
其好处是,通过引用访问对象时,不需要多一次的指针定位,使得访问速度更快。