1.对象的创建方式及是否调用构造方法
- new 无参/有参构造
- clone 不调用构造方法,由 JVM 创建新的对象并赋值字段。
- 反序列化 调用第一个没有实现序列化接口的父类的无参构造,如果没有,则调用 Object 类的无参构造
- Class.newInstance 无参构造,实际上是调用 5 的无参构造
- Constructor.newInstance(…args) 无参/有参构造
2.对象的创建流程
public class Test {
private int anInt = 1;
private static int staticInt = 2;
public void func(){
}
public static int staticFunc(){
return 6+9;
}
}
以 Test test = new Test();
为例
new 关键字创建 Test 对象,被 javac 编译器编译为三个字节码指令 new ,dup ,invokespecial。
new 关键字触发类的初始化阶段,如果该类是第一次使用,还未经历类的加载阶段。则先检查对象所属类型是否已经被加载到内存中。如果没有,则进行类的加载。
2.1.类的加载过程
1.加载阶段
记含有Test test = new Test();
代码的类为 C,通过加载类 C 的类加载器 CL,根据 Test 类的全限定名去获取描述该类的二进制字节流。对于任意一个类,都是根据加载它的类加载器及其本身来确定类的唯一性。所以我们可以让不同的类加载器,加载同一个类,那么方法区中会存在两个类型。类加载器不会直接尝试加载 Test 类,会遵循双亲委派模型,将加载请求递交给父加载器。直到父加载器无法找到目标类时,子加载器才会去加载 Test 类。
类加载器找到目标类后,首先进行文件格式验证包括验证魔数,验证版本号等
验证通过后,读取静态类型信息并存储在运行时数据区域->方法区(jdk8:Metaspace)中,以 instanceKlass 对象的形式存在。我们通过各种方式获取类型对应的 Class 对象时,由 JVM 在堆中创建。并通过 instanceKlass 中存储的 _java_mirror 指针找到 class 对象并返回。
instanceKlass 内容包括:类型信息,常量池,静态变量、即时编译后的字节码。
2.验证阶段
各种验证
3.准备阶段
为类中静态变量分配内存,并赋零值。
为类中 static final 修饰的常量(ConstantValue)分配内存并赋值。
4.解析阶段
将符号引用解析为直接引用。符号引用是指在编译期间,找到目标的唯一标识(Class_info、Fieldref_info、Methodref_info)。直接引用是指在运行期间,内存中找到目标的唯一标识(内存地址)
5.初始化阶段
由 JVM 执行类初始化方法< clinit>(),包括为静态变量赋值,执行静态代码块。
类加载阶段完成后,开始执行 new 字节码指令,创建对象,为对象分配内存空间。
2.2对象创建过程
1.分配内存
几乎所有实例都在堆上分配,而堆又是线程共享的。为了避免在并发情况下为对象分配内存时,多个线程同时操作同一块内存区域,需要有相应的线程安全方法。
为线程分配本地线程分配缓冲(Thread Local Allocate Buffer)TLAB,线程在自己的缓冲区中为对象分配内存,不够用再去申请,申请内存过程才需要同步锁定。
2.将分配到的内存空间赋零值(除对象头)
这一过程保证了,对象的实例成员变量在不赋初始值的情况下也有默认零值,可以直接被访问。
3.JVM 对对象头进行设置
JVM 中对象的存在形式为 Oop 对象。Oop 对象中分为1.对象头 2.实例数据 3.对齐填充
对象头包括 1.Markword(存储对象哈希码,锁标记等) 2.Metadata类型指针(指向方法区中的 instanceKlass 对象)
实例数据指对象从父类继承而来的实例数据 及 自己的实例成员变量。
JVM 在此阶段根据运行状态设置对象头内容。
到这里,对于 JVM 来说,一个对象已经创建好了。但对于java程序来说还没完,还需要对实例成员进行初始化。
下一步执行 invokespecial 字节码指令,执行实例初始化方法< init>()。
执行实例初始化方法< init>()
包括实例成员变量赋值,执行实例代码块,执行无参构造方法。
3.对象的内存布局
test 对象对应的堆中的 Oop 对象
对象头:mark :1 未锁定状态 + metadata :类型指针
实例数据: anInt:1
Test 类对应的方法区中的 instanceKlass 对象
instanceKlass 中的 _java_mirror 属性,就是 Test 类对应的 Class 对象。test.getClass() 实际上是通过 Oop 对象中的类型指针找到方法区中 instanceKlass 对象,通过 instanceKlass 找到 Class 对象。
类中的静态变量在 Class 对象末尾存储,staticInt:2
非私有实例方法(虚方法)在 instanceKlass 的 vtable 中存储。
vtable_len:6 虚方法表长度为6,是由于 vtable 中首先存储父类的 vtable, Test 的父类是 Object,Object 拥有 5 个虚方法(hashCode,equals,clone,finalize,toString)。
4.对象的访问定位
- 直接指针
通过栈帧中存储的 reference 数据保存的内存地址直接访问到堆中代表对象的 Oop 对象,再通过 Oop 对象中的类型指针找到方法区中代表类型的 instanceKlass 对象。
好处:快,一步到位
坏处:对象被GC移动时,得修改栈帧中 reference 的值
- 句柄
通过栈帧中存储的 reference 数据保存的句柄池中的句柄(堆中Oop对象内存地址+方法区instanceKlass类型对象内存地址)找到 Oop、instanceKlass 对象。
好处:对象被GC移动时,不需要修改栈帧中 reference 的值
坏处:较慢,需要经历两次寻址才能找到对象
5.对象在哪分配内存
java 中的所有实例都是在堆上分配内存的吗?
public void aMethod(){
Object anObject = new Object();
}
以上代码中,anObject 对象与 aMethod() 方法生命周期一致,方法被调用、方法栈帧被压入虚拟机栈时,对象存活。方法执行结束,栈帧弹栈时,栈帧销毁、对象不再使用。
如果 anObject 对象在堆上分配内存,因为堆被所有线程共享,在分配内存时需要进行同步。方法结束时,对象不再使用,需要由GC进行垃圾回收。
我们平时写代码时,类似以上代码中与方法声明周期一致的对象有很多,这些对象在同一时刻只会被一个线程访问,不会逃逸出线程之外。
试想,如果这些对象在栈上分配内存,那么即不用担心线程安全,因为虚拟机栈是线程私有的。也不用担心垃圾回收,因为对象可以随着栈帧的销毁而消亡,直接省去了垃圾回收的步骤。
何乐而不为呢?
5.1JVM 性能增强:逃逸分析
对实例的动态作用域进行分析,分析实例的逃逸范围,进行优化。
-
从不逃逸
对象在方法中定义,不传递到其他方法,与方法的生命周期相同。
-
方法逃逸
对象在方法中定义,作为参数传递到其他方法,被外部方法所引用。
-
线程逃逸
对象会被外部线程访问到,譬如赋值给可以在其他线程中访问到的实例成员变量。
1.栈上分配
如果一个对象符合从不逃逸的情况,与方法的生命周期一致。那么可以为这个对象在栈上分配内存。这样做的好处 1.线程安全 2.不需要垃圾回收
2.标量替换
不允许对象逃逸出方法范围内。
3.同步消除
如果可以确定一个对象不会被外部线程所访问,那么就可以对这个对象进行同步消除,消除同步措施。