对象创建过程
以简单对象的创建为例,说明对象创建过程,新建ObjectTest.java
,代码如下:
public class ObjectTest {
public static void main(String[] args) {
Person person = new Person("Json");
System.out.println(person.getName());
}
static class Person {
private String mName;
public Person(String mName) {
this.mName = mName;
}
public String getName() {
return mName;
}
public void setName(String mName) {
this.mName = mName;
}
}
}
随后执行javac ObjectTest.java
命令,编译该文件,生成ObjectTest.class
,使用javap -c ObjectTest.class
查看字节码,代码如下:
从字节码可以看出Person对象创建时,首先在code 0位置调用new关键词创建Person对象,随后在code 6位置使用invokespecial调用Person类的init函数,随后代码中就开始进行Person对象的属性获取,调用该对象的相关函数了,从这里来看一个Java对象创建应该分为两步:new指令 和 执行init函数。
那么问题来了,new指令到底干了什么,为什么执行玩就创建了一个对象,从代码中我们可以看到并没有声明init函数,那么这个函数又是从哪儿来的?
new指令
对于Java虚拟机而言,new指令意味着创建普通对象,该指令接受一个操作数,指向常量池中的一个引用,该引用用于指示要创建的对象类型,以前文代码为例,#2位置为new指令接受参数,其指向的是Person这个类,如下图
那么虚拟机中又是怎么响应new指令的呢?
在虚拟机中,处理new指令一般分为以下几步:
- 校验:检查类的符号引用,查看对象的类模版是否已经被加载(如果未加载会执行加载机制),校验字节码文件正确性。
- 分配内存:类加载校验完成后,类对象的相关信息已确认,此时就可以根据创建了类对象所需的大小来进行内存分配。
- 初始化:针对分配的内存进行初始化工作,包括静态变量赋初值并执行静态代码等。
- 设置:设置对象头,标记对象实例,Hash值,锁状态等信息,完成MarkWord和kClassPointer填充。
init函数
在new指令执行完成后,也就意味着在内存中我们拥有了一个类的实例对象,到此就会执行我们编写的构造函数,根据开发人员的设计进行初始化,使得对象的状态符合开发人员预期,init函数执行完成后,一个Java对象的创建也就完成了。
对象组成
在Hotspot虚拟机中,Java对象由对象头,实例数据,对齐填充三部分组成
- 对象头(Object Header):包含了关于堆对象布局,类型,GC状态,同步状态和标识哈希码等基本信息
- 实例数据(Instance Data):主要存放类的数据信息,包括父类信息,对象字段属性等
- 对齐填充(Padding):为了字节对齐,填充的数据,非必须
对象头
从HotSpot虚拟机官方文档中可以看到是这样描述对象头的:
object header
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.
大概含义是对象头是受GC管理的每一个对象的开头部分,其结构是通用的(每个oop都指向一个对象头)。对象头中包含有关堆对象布局,类型,GC状态,同步状态和身份哈希码的基本信息,其由两部分组成,在数组中,紧随其后的是一个长度字段。需要注意的是Java对象和VM内部对象都具有共同的对象头。
继续查找官方文档,我们可以看到对象头的两个部分分别为 klass pointer(类指针) 和 mark word(标记词) :
mark word
HotSpot虚拟机官方文档中这样描述Mark Word:
mark word
The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.
大概含义是 Mark Word是每个对象对象头的第一部分。通常是一组位域,包含同步状态和身份哈希码。也可以是指向同步相关信息的指针(具有特征低位编码)。在GC执行期间,有可能包含GC状态。
Mark Word在32位JVM中的长度是32位,在64位JVM中长度是64位。Mark Word在不同的锁状态下存储的内容不同,32位存储内容如下图:
64位存储内容如下:
从上面两张图可以看出,对于32或64位JVM中的Mark Word而言,虽然其数据占位长度有所差异,但其中组成内容基本是一致的:
- 锁标志位(lock):区分锁状态
- biased_lock:是否偏向锁
- 分代年龄:对象被GC的次数,次数到达阀值,对象就会被转移到老年代
- 对象的hashcode:运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中
- 偏向锁的线程id:偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作
- epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针
kclass pointer
HotSpot虚拟机官方文档中这样描述kclass pointer:
klass pointer
The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable".
大概含义是类指针是kclass pointer是每个对象对象头的第二部分。指向描述原始对象布局和行为的另一个对象(元对象)。对于Java对象而言,kclass中包含C++样式的“vtable”。
虚拟机正是通过这个指针来确定这个对象是那个类的实例。
对象引用方式
我们都知道对象在构造后,可以传递给其他对象,当然也可以在其他对象内构造持有该对象,此时我们可以称该对象被其他对象引用,因被其他对象引用后发生GC(见对象管理中的说明)时,是否可以回收该对象,又可以把引用分为强引用,弱引用,软引用,虚引用四种引用方式,各引用方式与是否可被GC回收如下图所示:
引用类型 | GC时是否可回收 | 备注 |
---|---|---|
强引用 | 不可回收 | 无论何时,只要强引用关系存在,则该对象不会被垃圾收集器回收 |
软引用 | 内存溢出前,选中软引用对象做二次回收 | 当软引用对象回收后,如果内存仍然不足,则会继续跑出内存溢出异常 |
弱引用 | 下次垃圾收集器工作时被回收 | 弱引用对象只能存活到下次垃圾收集器工作之前 |
虚引用 | 最弱的引用方式,虚引用完全不会对对象的生存时间造成影响 | 为对象设置虚引用关联的唯一目的是在这个对象被垃圾收集器回收时收到一个通知 |