深入了解JVM中的对象

1.对象的创建方式及是否调用构造方法

  1. new 无参/有参构造
  2. clone 不调用构造方法,由 JVM 创建新的对象并赋值字段。
  3. 反序列化 调用第一个没有实现序列化接口的父类的无参构造,如果没有,则调用 Object 类的无参构造
  4. Class.newInstance 无参构造,实际上是调用 5 的无参构造
  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.对象的访问定位

  1. 直接指针

通过栈帧中存储的 reference 数据保存的内存地址直接访问到堆中代表对象的 Oop 对象,再通过 Oop 对象中的类型指针找到方法区中代表类型的 instanceKlass 对象。

好处:快,一步到位

坏处:对象被GC移动时,得修改栈帧中 reference 的值

  1. 句柄

通过栈帧中存储的 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.同步消除

如果可以确定一个对象不会被外部线程所访问,那么就可以对这个对象进行同步消除,消除同步措施。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值