【Java八股面试系列】JVM-类和对象加载过程

时间:2024-02-15 14:44:08

目录

类和对象的加载过程

类的生命周期

类的加载过程

加载

验证

准备

解析

初始化

类卸载

对象的加载过程


类和对象的加载过程

什么是类加载和对象加载?

  1. 类加载(Class Loading):这是指JVM在运行时将类的字节码文件加载到内存中的过程。类加载的主要任务是找到类的字节码文件,然后读取它并存储在方法区中。这个过程是由类加载器完成的。

  2. 对象加载(Object Loading):这是对象在堆内存中分配空间的过程。当我们在代码中创建一个类的实例时,JVM会在堆内存中为这个对象分配空间,并将其实例化。这个过程是在类加载之后发生的。

简单来说,类加载是将类的字节码文件加载到内存中(Jvm虚拟机的方法区中),而对象加载是为这个类的实例在堆内存中分配空间并初始化。

类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。

这 7 个阶段的顺序如下图所示:

一个类的完整生命周期

类的加载过程

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

加载

主要目的就是使用二进制字节流将class文件加载进Jvm的方法区

类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流。

  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。

  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。

加载这一步主要是通过我们后面要讲到的 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破由双亲委派模型)。

验证

验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证的阶段因为比较关键,保证了后面的运行安全。这个阶段是比较耗费资源的,但是这个验证也是可以自己手动进行关闭的。如果我们的代码都是自己编写,并且引用的代码依赖都是自己使用过多次的,在生产环境的实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证阶段主要由四个检验阶段组成:

  1. 文件格式验证(Class 文件格式检查):基于二进制字节流进行实现,确保数据能够安全的加载到方法区之内。

  2. 元数据验证(字节码语义检查)

  3. 字节码验证(程序语义检查)

  4. 符号引用验证(类的正确性检查)

准备

准备阶段是正式为类变量(静态变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行初始化的都只是类变量,而不是实例变量。实例变量在对象进行初始化的时候都会被分配在堆中

  2. HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。

  3. 这个阶段设置的初始值只是每个类型的默认值,比如 static int a = 11;这个时候初始化了的a的值为0,而不是11。但是如果是final关键字修饰的变量,则直接会被赋值为设置的值。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

  • 虚拟机在类加载的解析阶段,会遍历类的常量池,查找所有的符号引用。

  • 对于每一个符号引用,虚拟机会尝试找到它引用的目标类、方法或字段。这可能需要加载其他的类或接口。

  • 如果找到了目标,虚拟机就会将这个符号引用替换为一个直接引用,这个直接引用可以直接访问目标类、方法或字段。

  • 如果找不到目标,或者目标不是一个有效的类、方法或字段(例如,尝试访问一个不存在的类或方法),那么解析过程就会失败,并且会抛出一个异常。

初始化

初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

说明:<clinit> ()方法是编译之后自动生成的。

对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. 当遇到 new、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。

    • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。

    • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。

    • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。

    • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。

  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。

  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。

  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。

  5. MethodHandleVarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用 就必须先使用 findStaticVarHandle 来初始化要调用的类。

类卸载

卸载类即该类的 Class 对象被 GC。

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。

  2. 该类没有在其他任何地方被引用

  3. 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

只要想通一点就好了,JDK 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

对象的加载过程

Java 对象的创建过程——五步

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

    new关键字时创建对象时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程。(加载将class文件读取到内存中,使用加载器进行加载)

  2. 为对象分配内存空间

    当类加载检查通过后,虚拟机会为新生对象分配内存空间,对象所需内存空间的大小在类加载完成后就已经确定了。为新生对象分配内存空间其实就是在Java堆中划分出一块确定大小的内存分配给新生对象。分配内存的方式有“指针碰撞”和“空闲列表”两种,选择哪种分配方式取决于Java堆内存是否规整。

    内存分配的两种方式

    • 指针碰撞 使用场合:堆内存规整(即没有内存碎片)的情况下。 实现原理:将用过的内存都整合到一边,没有用过的内存放到另一边,中间有一个分界指针,当需要为新对象分配内存空间时,只需要将分界指针向没有用过的内存一侧移动对象内存大小位置即可。

    • 空闲列表 使用场合:堆内存不规整的情况下。(JDK8默认的GR垃圾回收方式,是不规整的方式) 实现原理:虚拟机会维护一个列表,该列表记录了哪些内存是可用的,当需要为新对象分配内存空间时,只需要在列表中找一块足够大小的内存分配给对象实例,然后更新列表记录。 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器垃圾采用的垃圾收集算法

  3. 对象初始化零值

    内存分配完成后,虚拟机需要将新分配的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段可以在Java代码中可以不赋初始值就直接使用,程序能够访问这些实例字段的数据类型所对应的零值。

  4. 设置对象头

    初始化零值之后,虚拟机需要对对象头进行必要的设置,例如这个对象是哪个类的实例,如何才能找到这个类的元数据信息,对象的哈希码、GC 分代年龄、锁标志位、偏向锁标志位、线程持有的锁信息、对象是否可用等信息会存放到对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  5. 执行init方法

    执行完new指令后会接着执行init方法,将对象按照程序员的需求来进行初始化,这样一个真正可用的对象才算完全产生出来。

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

  1. 先加载父类;再加载本类;

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