一分钟掌握JVM——类的实例化过程

我们在调用构造函数创建一个对象时,JVM到底执行了什么?
这个问题看似简单却很少有人关注所以我就找了很多资料总结如下:

类的实例化
  1. 在常量池中查找是否存在代表这个类的符号引用,并检查这个符号引用所代表的类是否被加载、连接、初始化,如果没有就先执行类的加载、连接、初始化。

  2. 类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间无非就是从Java堆中划分出一块确定大小的内存而已。这个地方会有两个问题:

    (1)如果内存是规整的,那么虚拟机将采用的是指针碰撞法来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。

    (2)如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。如果垃圾收集器选择的是CMS这种基于标记-清除算法的,虚拟机采用这种分配方式。

    另外一个问题及时保证new对象时候的线程安全性。因为可能出现虚拟机正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就要在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁。虚拟机是否使用TLAB,可以通过-XX : +/- UseTLAB 参数来设定。

  3. 为分配的空间初始零值,如果使用TLAB , 这一工作过程也可以提前至TLAB分配时进行。这一操作保证了java对象可以不赋初始值,直接使用。

  4. 虚拟机对对象进行必要设置:例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。

以上操作执行完成后,对于JVM层面,对象已经创建成功了,new指令已经执行完成了,
但对于Java程序层面而言,对象的创建才刚刚开始……
  1. 为对象的变量赋予正确的值;根据程序中的代码块来赋值;
  2. 执行init方法,即按照构造函数进行赋值;
  3. 因此在构造器中可以使用this也可以调用方法;
  4. 注意:对象的实例化会先实例化父类那部分的对象方法或字段,并调用父类构造方法;
class Parent {
    // 静态变量
    public static String p_StaticField = "父类--静态变量";
    protected int i = 1;
    protected int j = 8;
    // 变量
    public String p_Field = "父类--变量";

    // 静态初始化块
    static {
        System.out.println(p_StaticField);
        System.out.println("父类--静态初始化块");
    }

    // 初始化块
    {
        System.out.println(p_Field);
        System.out.println("父类--初始化块");
    }

    // 构造器
    public Parent() {
        System.out.println("父类--构造器");
        System.out.println("i=" + i + ", j=" + j);
        j = 9;
    }
}

public class SubClass extends Parent {

    // 静态变量
    public static String s_StaticField = "子类--静态变量";

    // 变量
    public String s_Field = "子类--变量";

    // 静态初始化块
    static {
        System.out.println(s_StaticField);
        System.out.println("子类--静态初始化块");
    }
    // 初始化块
    {
        System.out.println(s_Field);
        System.out.println("子类--初始化块");
    }

    // 构造器
    public SubClass() {
        System.out.println("子类--构造器");
        System.out.println("i=" + i + ",j=" + j);
    }

    // 程序入口
    public static void main(String[] args) {
        new SubClass();
    }
}
* 执行结果:
父类--静态变量           
父类--静态初始化块
子类--静态变量
子类--静态初始化块
//上面是类的加载过程;
父类--变量
父类--初始化块
父类--构造器
i=1, j=8
//父类对象初始完成
子类--变量
子类--初始化块
子类--构造器
i=1,j=9
//子类对象实例化完成

Process finished with exit code 0

先加载父类后加载子类(同级静态代码的执行顺序与书写顺序有关),然后实例化(初始化构造)父类后实例化子类;

  • 对象定位方式

建立对象是为了使用对象,Java程序需要通过栈上的reference(引用)数据来操作堆上的具体对象。比如我们写了一句:

Object obj = new Object()
而new Object()之后其实有两部分内容,一部分是类数据(比如代表类的Class对象)、一部分是实例数据

由于reference在Java虚拟机规范中只是一个指向对象new Object()的引用obj,并没有规定obj应该通过何种方式去定位、访问堆中对象的具体位置,所以对象访问方式也是取决于虚拟机而定的。主流方式有两种:

1、句柄访问。Java堆中划分出一块句柄池,obj指向的是对象的句柄地址,句柄中则包含了类数据的地址和实例数据的地址

2、指针访问。对象中存储所有的实例数据和类数据的地址,obj指向的是这个对象

HotSpot虚拟机采用的是后者,不过前者的对象访问方式也是十分常见的。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值