JVM在“new”一个对象时做了什么

一、对象的创建过程总览

我们先对对象的创建过程进行一个总览:Java在new一个对象的时候,代码会解析成字节码指令交由JVM处理。JVM在遇到一条new指令时,大致会按以下步骤进行:

  • 检查 **new **指令的参数能够在常量池中定位到一个类的符号引用,没有则报错否则往下执行;
  • 接下来检查这个类是否被加载到内存中来,如果没有则需要先完成一个类加载过程,大致有以下步骤:
    • 加载:通过一个类的完全限定符查找此类的字节码文件并创建一个Class对象;
    • 链接(包括了以下三部分)
      • 验证:保证被加载的类的正确性;
      • 准备:为类的静态变量分配内存,并将其设置为默认值
      • 解析:把类中的符号引用转换为直接引用;
    • 初始化:执行类构造器<clinit>()方法,为类的静态变量赋予正确的初始值;
  • 在类是否加载的验证通过后,就会为我们new出来的这个对象分配内存空间;
  • 内容分配完成后,虚拟机必须将分配到的内存空间(不包括对象头)初始化为零值,如果用TLAB,则在TLAB分配时初始化为零值;
  • 设置对象头:主要设置类的元数据信息、对象的哈希码、对象的GC分代年龄的信息;
  • 执行<init>方法对对象按照程序员的意愿进行初始化。

在这里插入图片描述

二、类加载过程

1、加载

类的加载是指查找并加载类的二进制数据,具体是将.class文件中的二进制数据读取到内存中,并将其放在运行时的数据区的方法区内,之后再在方法区(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放置在方法区中)创建一个Class对象,用在封装类在方法区内的数据结构。

Java虚拟机可以通过以下多种方式加载.class文件:

  • 从本地系统中加载 .class 文件(最常用的方式);
  • 通过网络下载 .class 文件
  • zipjar 等归档文件中加载 .class 文件
  • 从专有数据库中提取 .class 文件
  • 将 Java 源文件动态编译成为 .class 文件(动态代理)。

类的加载是由类的加载器完成的,类的加载器分为两种:

  1. Java虚拟机自带的加载器,包括类加载器,扩展类加载器以及系统类加载器;
  2. 用户自定义加载器,为ClassLoader类的子类,用户可以通过实现该类来定义类的加载方式。

2、链接

类被加载后,就进入了链接阶段,链接就是将以及读入到内存中的类的二进制文件合并到虚拟机的运行环境中去。链接主要包括三个阶段,分别时验证、准备和解析阶段:

  • 验证主要是确保被加载的类的正确性;
  • 准备阶段则是为类的静态变量分配内存,并将其值设置为默认值;
  • 解析阶段主要任务是把类中的符号引用转换为直接引用

2.1 验证阶段

类的验证阶段的主要目的是确定Java类的二进制数据的正确性,因为Java虚拟机并不知道.class文件是如何被创建的,有可能是正常创建的,也有可能是黑客特制破坏虚拟机的,所以要有验证环节,提高代码的健壮性。

  • 类文件的结构检查:确保类文件遵循 Java 类的固定格式;
  • 语义检查:确保类本身符合 Java 语法规定,如验证 final 修饰的类是否有子类、final修饰的方法是否被重写;
  • 字节码验证:确保字节码流可以被 Java 虚拟机正确的执行,它是由操作码的单字节指令组成的序列,每一个操作码都跟着一个或多个操作数,Java 虚拟机会验证该操作数是否合法;
  • 二进制兼容验证:确保类与类之间引用的协调性;如A类中a方法引用B类中b方法,虚拟机会验证A类时会检查方法区内是否有B类的b方法,如果不存在或不兼容时,会抛出 NoSuchMethodError 异常。

2.2 准备阶段

为类的静态变量分配内存空间,并将其赋予默认值,注意是初始值,如 static int 类型初始值为 0。

2.3 解析阶段

该阶段主要是将类中的符号引用转换为直接引用,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符类符号引用进行。

如在 A 类中 a 方法中引用了 B 类中的 b 方法,将 b 方法放入 A 类中。

3、初始化

3.1 首次主动使用

当Java程序首次主动使用一个类时,JVM虚拟机才会执行类的初始化,以下情况总结了类的首次使用:

  • 创建类的实例;
  • 访问某个类或接口的静态变量,或者对该静态变量赋值;
  • 调用类的静态方法;
  • 反射(如Class.forName("com.jojo.test.Demo"));
  • 初始化一个类的子类;
  • Java虚拟机启动时被标明为启动类的类;
  • JDK1.7 开始提供的动态语言支持:java.lang.invoke.MethodHandle 实例的解析结果 REF_getStaticREF_putStaticREF_invokeStatic句柄对应的类没有初始化,则初始化。

除了以上7种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。

特别注意:

调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

3.2 初始化过程

初始化过程:为静态变量赋值 ——>执行static代码块

特别注意:

static代码块只有 jvm 能够调用。如果是多线程需要同时初始化一个类,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

因为子类存在对父类的依赖,所以**类的加载顺序是先加载父类后加载子类,初始化也一样。**不过,父类初始化时,子类静态变量的值也有有的,是默认值。

最终,方法区会存储当前类类信息,包括类的静态变量类初始化代码定义静态变量时的赋值语句静态初始化代码块)、实例变量定义实例初始化代码定义实例变量时的赋值语句实例代码块构造方法)和实例方法,还有父类的类信息引用。

三、内存分配

1、内存划分

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存块从Java堆中划分出来。

  • 指针碰撞

    假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞(Bump The Pointer)”。

  • 空闲列表

    如果Java堆张的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配额你存的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表(Free LIst)”。

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,即简单又高效;而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。

2、线程安全问题

对象创建在虚拟机中时非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

解决这个问有两种可选方案:

  1. 对分配内存空间的工作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;

  2. 另一种方式是把内存分配的动作按照线程划分在不同的空间之中进行,既每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。

    虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来决定。

四、初始化零值与对象头设置

1. 初始化零值

内存分配完成后,虚拟机必须将分配到的空间(不包括对象头)都初始化为零值,如果使用TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。

这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。

2. 对象头设置

接下来Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

五、<init>方法

在上面工作都完成后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角来看,对象创建才刚刚开始——构造函数,即Class文件中的<init>()方法还没有执行。

一般来说,new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

六、参考资料

《深入理解Java虚拟机》-周志明

猜你喜欢

转载自blog.csdn.net/Allen_Adolph/article/details/106768604