jvm-对象的创建过程

在java中使用new创建对象时,虚拟机创建的过程:

以Object obj = new MyObject();为例(其他创建实例方式也一样)

一、分配内存

1、虚拟机会检查MyObject这个类是否存在,有没有被加载。如果MyObject没有被加载过,那么就先加载这个类;

2、虚拟机根据MyObject类的类信息在堆中分配内存空间,分配内存有两种方式:

(1)指针碰撞:当内存是规整的,此时有一个指针,在该指针一侧的内存是已经被分配了,而另一侧的内存则是空闲的,当创建对象要分配内存时,指针向空闲内存的一侧移动与要创建的对象大小相等的距离,这段距离之间的内存就用于存放要创建的对象。

(2)空闲列表:内存很少会是规整的,大多都是东一块西一块的的破碎内存,此时使用一个列表把空闲的内存记录下来,当要创建对象时,找出最合适的一块空闲内存用于存储这个要创建的对象。

以上两种方法在单线程中是没有问题的,但是,当处于多线程的时候,“指针碰撞”方式,在一个线程中创建第一个对象,读取了指针的地址,正要把指针向空闲内存一侧移动时,第二个线程也要创建第二个对象,此时指针还没有被修改,所以在第二个线程中读取的指针还是原来的地址,然后分配内存也是从那个地址把指针向空闲的一侧移动等于第二个对象大小的距离。。。这个时候,第一个线程创建的第一个对象,与第二个线程创建的第二个对象 会相互覆盖,数据就会出错。

而解决方法有两个:

(1)创建对象的时候进行同步操作,这样就不会出现多个线程读取到的指针是相同的情况了。。但是,同步会导致效率低下。

(2)使用TLAB本地线程分配缓冲,也就是每个线程在创建时都先分配一块堆内存用作缓冲,在哪个线程创建的对象都在这个线程所属的TLAB中创建对象。

3、分配完对象的内存空间后,把这块内存的值置为零置,这样的话,对象中没有被初始化的实例字段就存在初始值了,对象变量字段初始值为null,基本类型为对应的零值。。

4、初始化对象头数据,

对象头数据分为: 对象的运行时数据和类型指针;

    对象运行时数据:GC年龄分代、锁状态标志、线程持有的锁、hashcode哈希码等。

    类型指针:指向方法区中类型信息的指针。类型信息就是编译后的代码以及其他关于这个类的数据信息。

二、初始化对象

1、调用构造方法进行初始化。

三、赋值

1、给变量赋值,新对象的引用赋给变量。

注意:

其中二、三两个步骤可能会因为jvm的优化导致指令重排序,正常步骤是一、二、三,jvm优化后可能就变成一、三、二。

指令重排序在单线程的时候是很好的优化,但是在多线程的时候可能就会出现问题了。

例子:

public class Singleton {  
    private static volatile Singleton instance;  
    private Singleton (){}  
    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized(Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }            
            }
        }  
        return instance;  
    }  
}

在双重检验的单例模式中,多线程调用静态方法获取单例,线程A执行完一和三后,单例字段就不会是null了,最后执行创建对象的第三个步骤---即调用构造方法初始化对象,如果构造方法内执行的时间比较长,此时线程B也调用该单例执行静态方法,遇到第一个if (instance == null)时,instance == null的值会返回false,这样的话,就不会执行if内的同步代码块,如果线程A创建对象还没把构造方法执行完,线程B调用该静态方法就已经获取单例对象成功了,但是,此时该单例对象虽然不为null,但是却还没有初始化成功,这样的话,单例就有问题。

所以,为了解决这个问题,就要给单例字段加volatile修饰,防止jvm进行指令重排序。另外,synchronized只保证释放锁之前把对共享变量的修改刷到主内存,但在执行代码期间的共享变量的可见性还是无法保证的。。所以加volatile也是保证了 instance = new Singleton();能够被其他线程立即可见,

可以参考双重检验的单例模式,为什么要用volatile关键字 - 甜菜波波 - 博客园

### JVM对象创建过程Java 程序通过 `new` 关键字来创建一个新的对象实例时,JVM 需要经历一系列复杂的操作才能使这个对象成为可使用的状态。具体来说: #### 1. 类型检查与加载 在执行任何其他动作之前,JVM 必须先确认该类型的定义已经被加载到了内存中。如果尚未加载,则会触发类加载器去查找并加载相应的 `.class` 文件。 #### 2. 内存分配 一旦类型信息被成功加载,下一步就是为新对象分配足够的连续空间用于存储其成员变量和其他必要的元数据。这部分工作通常由垃圾回收器管理的堆区负责完成[^1]。 ```java // 假设有一个简单的Person类 public class Person { private String name; } ``` 对于上述代码中的 `Person` 类,在创建新的 `Person` 实例时,JVM 将为其分配适当大小的空间以容纳 `name` 字段以及其他潜在的数据结构。 #### 3. 构造函数调用前的状态设置 此时虽然已经预留好了物理上的位置给即将诞生的新实体,但从逻辑层面看它仍然是未初始化的状态;即所有字段均保持默认值(如数值型为0,布尔型为false,引用类型则为空null),直到显式的赋值行为发生为止[^3]。 #### 4. 执行 `<init>` 方法 紧接着便是最重要的一步—-构造方法(`<init>`) 的实际调用了。这期间可能会涉及多个层次的操作: - 如果存在父类的话,那么子类构造器内部隐含着对其超类构造器的一次自动调用; - 用户自定义的一些初始化语句也会被执行; - 同样地还有静态/非静态初始块内的表达式求值等。 值得注意的是,只有当整个链路顺利完成之后,才会返回指向新建好且已配置完毕的对象引用给原始发起者。 ```java // 定义带参数构造器的例子 public class Employee extends Person{ public Employee(String n){ super(); // 调用父类无参构造器 this.name=n; // 设置员工姓名 } } ``` 在这个例子中,当我们尝试构建一个具体的 `Employee` 对象时,除了自身的初始化外还会依次向上追溯至最顶层基类 `Object` 来确保每一个环节都被妥善处理过。 #### 5. 返回对象引用 最后,经过前面几步精心打造出来的成品会被封装成一个有效的引用传递回最初发出请求的地方供后续使用。 综上所述,尽管表面上看起来只是简单的一个 `new` 操作符的应用,背后却隐藏着许多细致入微的工作流程保障了最终产物的质量和安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值