JVM - 类的生命周期

339 阅读6分钟

本文个人博客地址:www.leafage.top/posts/detai…

生命周期:

一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析统称为连接。

一、加载:

  1. 通过类的全限定名,将类的 .class 文件中的二进制数据读到内存中;
  2. 将其所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成代表该类的java.lang.Class对象,作为方法区该类的访问入口;

二、连接:

类加载分为三个步骤:验证 —> 准备 —> 解析,连接的过程是在类加载快结束时就已经开始,它和加载是交叉进行的。

1. 验证:

目的: 确保Class文件字节流包含的信息中符合《Java虚拟机规范》的全部约束要求,保证这些信息被作为代码运行时,不会危害虚拟机自身的安全。 验证的内容主要有: 1. 文件格式验证:是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理; 2. 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java虚拟机规范》的要求; 3. 字节码验证:通过数据流分析和控制流分析,确定程序的语义是合法的、符合逻辑的; 4. 符号引用验证:对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验;

2. 准备:

为类中定义的变量(静态变量)分配内存(在运行时数据区中的方法区中),并设置初始值。

3. 解析(某些情况下可以在初始化阶段之后再开始):

  1. 将常量池内的符号引用替换为直接引用的过程;
  2. 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符这7个符号引用;

三、初始化:

就是执行类构造器 () 方法的过程,这个方法是 javac 编译器的自动生成物(由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的),不是程序员在 Java 代码中直接编写的。

《Java虚拟机规范》严格规定了有且只有以下六种情况(被称为对一个类型的主动引用)必须立即对类进行初始化:

  1. 遇到new、getstatic(读取静态字段)、putstatic(设置静态字段)、invokestatic(调用静态方法)这四条字节码指令时(读取和设置静态字段时,被final修饰、已在编译器把结果放入常量池的静态字段除外);
  2. 使用java.lang.reflect包的方法对类型进行反射调用时;
  3. 当初始化过程中,如果父类没有进行初始化时;
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(含有main方法的类),虚拟机会先初始化这个主类;
  5. 当使用JDK7新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,且这个方法句柄对应的类没有进行初始化时;
  6. 当一个接口中定义了JDK8新加入的默认方法(default修饰)时,如果这个接口的实现类发生了初始化,那这个接口要在其之前被初始化;

除了以上6种情况,其他方式的引用都不会出发初始化,被称为被动引用(例如:通过数组定义来引用类,不会触发此类的初始化;通过子类引用父类的静态字段,子类不会进行初始化);

四、使用

对象的创建:

  1. 当 Java 虚拟机遇到一条字节码 new 指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有那就先执行相应的类加载过程;

  2. 当类加载检查通过后,加下来虚拟机将为新生对象分配内存; 内存分配有两种方式:

    • 指针碰撞: 假设 Java 堆中内存是绝对规整的,所有被使用过的内存都放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那分配就仅仅是把那个指针向空闲方向挪动一段与对象大小相等的距离;
    • 空闲列表: 假设 Java 堆中内存不是规整的,已被使用过的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就需要维护一个列表,记录上那些内存块是可用的,再分配内存时就从列表中找到一块足够大的内存空间分配给对象实例,并更新列表上的记录;
  3. 为对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息;

  4. New 之后会接着执行 () 方法,按照程序设计对对象进行初始化;

对象创建的并发问题:

  • 方案一:采用CAS配上失败重试的方式保证更新操作的原子性;
  • 方案二:内存分配的动作按照线程划分在不同的空间中进行,即每个线程在 Java 堆中预先分配一块小内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓冲区时才需要同步锁定; 虚拟机是否使用 TLAB,可以通过 -XX:+/-UseTLAB 参数来设定;

五、卸载

由java虚拟机自带的加载器加载的类,在虚拟机的生命周期中,始终不会被卸载 ,因为java虚拟机本身会一直使用用这些类加载器,而这些类加载器会一直引用它们加载的类的Class对象,因此这些类不会被卸载。

由用户自定义的类加载器加载的类是可以被卸载的。一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。