编程语言
首页 > 编程语言> > 【JVM技术探索】全流程化分析Java对象的创建过程

【JVM技术探索】全流程化分析Java对象的创建过程

作者:互联网

前言概要

对应过程则是:对象创建、对象内存布局、对象访问定位的三个过程。
【JVM技术探索】全流程化分析Java对象的创建过程

对象的创建过程

【JVM技术探索】全流程化分析Java对象的创建过程

对象的创建方式

java中对象的创建方式有很多种,常见的是通过new关键字和反射这两种方式来创建。除此之外,还有clone、反序列化等方式创建


通过new关键字创建
// Person zhangsan = new Person(id, height, weight)
Person zhangsan = new Person();

通过反射创建

反射创建对象,可以通过class.newInstance()调用无参的构造器创建对象,也可以使用构造器来创建constructor.newInstance()


//Class clz = Class.forName("Person类的全限定类名")
Class clz = Person.class;
Person zhangsan = clz.newInstance()
// 使用构造器创建
Constructor<Person> cons = clz.getConstructor()
// 也可以指定参数类型获取有参构造器
Person zhangsan1 = cons.newInstance()

通过clone创建对象

当类实现了Cloneable接口时,可以使用clone()方法复制一个对象。需要留意是clone方法是浅拷贝。

Person libo = new Person(name: "李博", age:12, ...)
Person Livonor = new Person(name: "Livorno", age:32, ...)
libo.setFather(Livonor)
Person zhangsi = libo.clone() // 此时,张四和张三的名字、父亲在内存中都引用了相同的对象
反序列化创建

通过读取IO数据流创建,非本节重点

对象的创建过程

检查类加载(包含是否初始化、是否被加载、是否被解析)

对于new和反射两种创建方式而言,需要检查创建对象所使用的参数是否已完成类加载(比如它的类型和参数类型)。如果没有,要先完成类加载过程

分配内存空间

【JVM技术探索】全流程化分析Java对象的创建过程

指针碰撞

【JVM技术探索】全流程化分析Java对象的创建过程

指针碰撞的方式是假设内存空间是规整的,被使用的和空闲的内存被分割成了两整块,通过一个指针记录分界点。在给对象分配内存的时候,将指针空闲区域移动一段与对象大小相等的距离即可。

空闲列表

【JVM技术探索】全流程化分析Java对象的创建过程

如果内存不规整,那么就需要维护一张表,来记录内存中那些地址是空闲的。分配对象时,通过空闲列表去找到一块足够大的空闲内存分配给对象并更新空闲列表。

多个线程创建的对象内存的冲突

举个例子,线程1和2同时要创建两个对象,指针是同一个。它们各自将指针加载到了cpu缓存,然后去执行分配地址空间的指令。结果就导致,后分配的哪一个,可能将先分配的那个对象的地址给覆盖了。

解决的办法有两种,一种是对分配内存的动作进行同步处理,即采用CAS加失败重试的方式,保证更新操作的原子性

// 伪代码表示CAS+失败重试
while(true){
    oldPtr = ptr //读取共享指针
    newPtr = oldPtr + sizeOfInstance
    if(compareAndSet(oldPtr, ptr, newPtr)){break}
}

另一种是使用TLAB的方式将线程的分配空间在堆内存中隔离开,在堆中为每个线程预先分配一小块不同的空间,每个线程创建对象都在自己对应的空间中完成

即每个线程在 Java堆中预先分配一小块内存(本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。


分配完内存之后,对象就已经存在于虚拟机的堆中了,此时虚拟机要将分配的内存空间初始化为零值(对象头例外)。

设置对象头

对象头包含了两种信息:MarkWord和类型指针。

执行对象实例构造函数

首先递归的执行父类的构造函数,然后收集本类中为实例变量赋值的语句并执行,最后执行构造方法中的语句

public class AddA {
    public static void main(String[] args) {
        Father guy = new Son(30);
        guy.saySomething();
        System.out.println(guy.age);
    }
}

class Father{

    int age = 60;

    public Father() {
        saySomething();
    }

    public Father(int age) {
        this.age = age;
    }

    public void saySomething(){
        System.out.println("I am the father, " + age + "years old");
    }
}

class Son extends Father{

    int age = 20;

    public Son(int age) {
        // super();  不写则隐式调用方法,写则必须在子类构造方法的第一句
        saySomething();
        this.age = age;
        saySomething();
    }

    public void saySomething(){
        System.out.println("I am the son, " + age + " years old");
    }
}

因为它涉及到了多态与方法的动态分派。在这里先简单描述一下它的执行过程,用来掌握构造方法的执行还是ok的。

由于多态的规则:被重写的方法使用动态分派,查找(vtable)方法表,该方法实际是属于子类对象的

因此guy.saySomething()实际调用的是子类对象的方法,打印出第四句话,I am the son, 30 years old。

最后,输出guy.age. 成员变量不具备多态性,因此打印出父类对象的age 60.

I am the son, 0 years old
I am the son, 20 years old
I am the son, 30 years old
I am the son, 30 years old

对象的内存布局

【JVM技术探索】全流程化分析Java对象的创建过程

对象在堆中的存储布局划分为三个部分:对象头、实例数据和对其填充(padding)

【JVM技术探索】全流程化分析Java对象的创建过程

对象的内存布局

对象头

对象头中包含markword(标记字段)和类型指针【数组长度】。

markword

markword存储与对象自身定义数据无关的信息,用来表示对象的运行时状态。包括了HashCode,GC年龄,锁状态等信息。在一个32位的虚拟机中,markword用一个32位的bitmap表示,bitmap最后两位存放锁状态信息,如下图。

【JVM技术探索】全流程化分析Java对象的创建过程

指向类型元数据,从而可以通过对象来访问到它的类型信息

主要记录数组的长度信息一般为4字节(根据int的范围来考虑)

实例数据

即代码中定义的字段内容

注:这部分数据的存储顺序会受到虚拟机分配参数(FieldAllocationStyle)和字段在Java源码中定义顺序的影响。

// HotSpot虚拟机默认的分配策略如下:

longs/doubles、ints、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers)

CompactFields = true;

// 如果 CompactFields 参数值为true,那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。

对齐填充

如果对象的实例数据占用空间不是8的整数倍,则填充0值让对象的占用空间位8的整数倍。

【JVM技术探索】全流程化分析Java对象的创建过程

对象的访问定位

常见的有两种方式,句柄访问和直接指针访问。

【JVM技术探索】全流程化分析Java对象的创建过程

句柄访问

【JVM技术探索】全流程化分析Java对象的创建过程

【JVM技术探索】全流程化分析Java对象的创建过程

标签:Java,对象,子类,age,流程化,Person,JVM,创建,指针
来源: https://blog.51cto.com/alex4dream/2856360