Java对象创建过程

2年前 (2022) 程序员胖胖胖虎阿
226 0 0

文章目录

  • 一、对象的组成
    • 1. 对象头
      • ① MarkWord
      • ② 指向类的指针
      • ③ 数组长度
    • 2. 实例数据
    • 3. 对齐填充
  • 二、对象创建方式
    • 1. new关键字
    • 2. Class类的newInstance方法
    • 3. Constructor类的newInstance方法
    • 4. Object类的clone方法
    • 5. 反序列化
  • 三、对象创建过程
  • 四、创建过程举例
    • 1. 无父类的对象创建
    • 2. 有父类的对象创建

一、对象的组成

对象的组成包含三部分:对象头、实例数据、对齐填充。
Java对象创建过程

1. 对象头

Java的对象头由以下三部分组成:MarkWord指向类的指针数组长度(只有数组对象才有)

① MarkWord

MarkWord包含:哈希码、GC分代年龄、锁标识状态、
线程持有的锁、偏向线程ID(一般占32/64 bit)。

MarkWord记录了对象锁相关的信息。

当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和MarkWord有关。

MarkWord在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

32位JVM中,MarkWord在不同的锁状态下存储的内容:Java对象创建过程
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

锁升级流程:

  1. 当对象没有锁时,这就是一个普通的对象,MarkWord记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。锁状态为无锁
  2. 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
  3. 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,MarkWord中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
  4. 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是MarkWord中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把MarkWord里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
  5. 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁MarkWord的指针,同时在对象锁MarkWord中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把MarkWord中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
  6. 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
  7. 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

② 指向类的指针

Java对象的类数据保存在方法区。

该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

③ 数组长度

只有数组对象保存了这部分数据。

该数据在32位和64位JVM中长度都是32bit。

2. 实例数据

对象的实例数据就是在Java代码中能看到的属性和他们的属性值

3. 对齐填充

因为JVM要求Java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。

二、对象创建方式

1. new关键字

最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象。

public static void main(String[] args) {
    User = new User();
}

2. Class类的newInstance方法

通过Java的反射机制使用Class类的newInstance方法来创建对象。这个newInstance方法调用无参的构造器创建对象。

public static void main(String[] args) throws Exception {
    // 方法1
    User user1 = (User)Class.forName("com.joker.pojo.User").newInstance();
    // 方法2
    User user2 = User.class.newInstance();
}

事实上Class的newInstance方法内部调用的是Constructor的newInstance方法。

3. Constructor类的newInstance方法

通过Java的反射机制使用Constructor类的newInstance方法来创建对象。

java.lang.relect.Constructor类里的newInstance方法比Constructor类的newInstance方法更加强大些,我们可以通过这个newInstance方法调用有参数的和私有的构造函数。

public static void main(String[] args) throws Exception {
    Constructor<User> constructor = User.class.getConstructor(Integer.class);
    User user3 = constructor.newInstance(123);
}

4. Object类的clone方法

通过实现Cloneable接口,重写Object类的clone方法来创建对象(浅拷贝)。

Java为所有对象提供了clone方法(Object类),又出于安全考虑,将它设置为了保护属性。

	protected native Object clone() throws CloneNotSupportedException;

我们可以通过反射(reflect)机制在任意对象中调用该方法。

如果不通过反射的方式,我们要如何实现对象克隆呢?可以通过实现Cloneable接口,重写Object类的clone方法来实现对象的克隆。

实现原理:
Java API采用判断是否实现空接口Cloneable的方法来判断对象所属的类是否支持克隆。如果被克隆对象所属类没有实现该接口,则抛出NotDeclareCloneMethod 异常。当支持克隆时,通过重写Object类的clone方法,并把方法的修饰符改为public,就可以直接调用该类的实例对象的clone方法实现克隆。

我们常用的很多类都是通过这种方式来实现的,如:ArrayList、HashMap等。

@Data
public class User implements Cloneable {
    private String id;
    private String userName;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User();
        User user1 = (User)user.clone();
    }
}

5. 反序列化

当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。

public static void main(String[] args) throws Exception {
    User user = new User();
    user.setId("1");
    user.setUserName("haha");
    // 写对象
    ObjectOutputStream output = new ObjectOutputStream(
            new FileOutputStream("F:\\joker\\text.txt"));
    output.writeObject(user);
    output.close();

    // 读对象
    ObjectInputStream input = new ObjectInputStream(new FileInputStream(
            "F:\\joker\\text.txt"));
    User user1 = (User) input.readObject();
}

三、对象创建过程

这里以new关键字方式创建对象为例。

对象创建过程分为以下几步:

  1. 检查类是否已经被加载;

    new关键字时创建对象时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程。类的加载过程需要经历:加载、链接、初始化三个阶段。

    具体过程可参考文章:Java类的加载机制

  2. 为对象分配内存空间;

    此时,对象所属类已经加载,现在需要在堆内存中为该对象分配一定的空间,该空间的大小在类加载完成时就已经确定下来了。

    为对象分配内存空间有两种方式:

    1. 第一种是jvm将堆区抽象为两块区域,一块是已经被其他对象占用的区域,另一块是空白区域,中间通过一个指针进行标注,这时只需要将指针向空白区域移动相应大小空间,就完成了内存的分配,当然这种划分的方式要求虚拟机的对内存是地址连续的,且虚拟机带有内存压缩机制,可以在内存分配完成时压缩内存,形成连续地址空间,这种分配内存方式成为“指针碰撞”,但是很明显,这种方式也存在一个比较严重的问题,那就是多线程创建对象时,会导致指针划分不一致的问题,例如A线程刚刚将指针移动到新位置,但是B线程之前读取到的是指针之前的位置,这样划分内存时就出现不一致的问题,解决这种问题,虚拟机采用了循环CAS操作来保证内存的正确划分。
    2. 第二种也是为了解决第一种分配方式的不足而创建的方式,多线程分配内存时,虚拟机为每个线程分配了不同的空间,这样每个线程在分配内存时只是在自己的空间中操作,从而避免了上述问题,不需要同步。当然,当线程自己的空间用完了才需要需申请空间,这时候需要进行同步锁定。为每个线程分配的空间称为“本地线程分配缓冲(TLAB)”,是否启用TLAB需要通过 -XX:+/-UseTLAB参数来设定。
  3. 为对象的字段赋默认值;

    分配完内存后,需要对对象的字段进行零值初始化(赋默认值),对象头除外。

    零值初始化意思就是对对象的字段赋0值,或者null值,这也就解释了为什么这些字段在不需要进程初始化时候就能直接使用。

  4. 设置对象头;

    对这个将要创建出来的对象,进行信息标记,包括是否为新生代/老年代,对象的哈希码,元数据信息,这些标记存放在对象头信息中。

  5. 执行实例的初始化方法lint

    linit方法包含成员变量、构造代码块的初始化,按照声明的顺序执行。

  6. 执行构造方法。

    执行对象的构造方法。至此,对象创建成功。

上述为无父类的对象创建过程。对于有父类的对象创建过程,还需满足如下条件:

  1. 先加载父类;再加载本类;
  2. 先执行父类的实例的初始化方法init(成员变量、构造代码块),父类的构造方法;执行本类的实例的初始化方法init(成员变量、构造代码块),本类的构造方法。

四、创建过程举例

1. 无父类的对象创建

对象创建源码

public class ClassA {

    private static int y = 1;
    private static String s = "1";

    static {
        y=2;
    }

    private static int x = 1;
    
    static {
        s="2";
    }
    
    static {
        x=2;
    }

    public ClassA() {
        x = x+1;
        y = y+1;
        s = "3";
    }

    public static void main(String[] args) {
        ClassA classA = new ClassA();
    }
}

具体创建步骤

  1. 类未加载,先加载类;

    1. 链接阶段时,准备阶段,为静态变量赋默认值

      y = 0;
      s = null;
      x = 0;
      
    2. 初始化阶段时,为静态变量赋初始值(执行类的初始化方法clinit);

      clinit方法包含静态变量、静态代码块,按照声明的顺序执行。

      y = 1;
      s = "1";
      y = 2;
      x = 1;
      s = "2";
      x = 2;
      
  2. 为成员变量赋默认值;

    aa = 0;
    bb = 0;
    
  3. 对象初始化,为成员变量赋初始值。

    执行实例的初始化方法lint(成员变量、构造代码块)。

    aa = 1;
    aa = 2;
    bb = 1;
    bb = 2;
    
  4. 执行构造方法。

    aa = 3;
    bb = 3;
    

至此,对象创建完成。

各属性值情况如下:
Java对象创建过程

2. 有父类的对象创建

对象创建源码

父类

@Data
public class ClassParent {

    public static int p1 = 1;

    public int p2 = 1;
    {
        p2 = 2;
    }
    static {
        p1 = 2;
    }
    
    public ClassParent() {
        p2 = 3;
    }
}

子类

@Data
public class ClassChild extends ClassParent {

    private static int c1 = 1;

    private int c2 = 1;
    {
        c2 = 2;
    }
    static {
        c1 = 2;
    }

    public ClassChild() {
        super();
        c2 = 3;
    }

    public static void main(String[] args) {
        ClassChild classA = new ClassChild();
    }
}

具体创建步骤

  1. 类未加载,先加载类;

    先加载父类,再加载子类

    1. ClassParent类加载:链接阶段时,准备阶段,为静态变量赋默认值;
      p1 = 0;
      
    2. ClassParent类加载:初始化阶段时,为静态变量赋初始值(执行类的初始化方法clinit);
      p1 = 1;
      p1 = 2;
      
    3. ClassChild类加载:链接阶段时,准备阶段,为静态变量赋默认值;
      c1 = 0;
      
    4. ClassChild类加载:初始化阶段时,为静态变量赋初始值(执行类的初始化方法clinit);
      c1 = 1;
      c1 = 2;
      
  2. 为成员变量赋默认值;

    这里的父类之类执行顺序没去验证,个人认为是先父类后子类。

    1. ClassParent类:为成员变量赋默认值;
      p2 = 0;
      
    2. ClassChild类:为成员变量赋默认值;
      c2 = 0;
      
  3. ClassParent类:对象初始化,为成员变量赋初始值。

    p2 = 1;
    p2 = 2;
    
  4. ClassParent类:执行构造方法。

    p2 = 3;
    
  5. ClassChild类:对象初始化,为成员变量赋初始值。

    c2 = 1;
    c2 = 2;
    
  6. ClassChild类:执行构造方法。

    c2 = 3;
    

至此,对象创建完成。

各属性值情况如下:
Java对象创建过程

https://blog.csdn.net/justloveyou_/article/details/72466416

版权声明:程序员胖胖胖虎阿 发表于 2022年9月12日 上午8:32。
转载请注明:Java对象创建过程 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...