虚拟机类加载机制

Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为虚拟机的类加载机制。

类加载时机

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

《Java 虚拟机规范》严格规定了有且只有六种情况必须立即对类进行初始化:
①、遇到 new、 getstatic、 putstatic、 invokestatic 这四条字节码指令,能够生成这四条指令码的典型 Java 代码场景有:

  • 使用 new 关键字实例化对象时;
  • 读取或设置一个类型的静态字段时(被 final 修饰,已在编译期把结果放入常量池的静态字段除外);
  • 调用一个类的静态方法时。
    ②、使用 java.lang.reflect 包的方法对 Class 进行反射调用时,如果类型没有进行过初始化、则需要触发其初始化;
    ③、当初始化类时,如发现其父类还没有进行过初始化、则需要触发其父类进行初始化;
    ④、当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
    ⑤、当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后解析的结果为 REF_getStatic , REF_putStatic , REF_invokeStatic , REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化;
    ⑥、当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。

类加载过程

1. 加载

在加载阶段,虚拟机需要完成以下三件事:

  • 通过一个类的全限定名来获取定义此类的二进制字节流 ;
  • 将这个字节流所代表的静态存储结构转换为运行时数据结构;
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为这个类的各种数据的访问入口。
    《Java 虚拟机规范》并没有限制从何处获取二进制流,因此可以从 JAR 包、WAR 包获取,也可以从 JSP 生成的 Class 文件等处获取。
2. 验证

这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,从而保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
验证阶段大致会完成下面四项验证:

  • 文件格式验证 :验证字节流是否符合 Class 文件格式的规范;
  • 元数据验证 :对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java 语言规范》的要求(如除了 java.lang.Object 外,所有的类都应该有父类);
  • 字节码验证 :通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的(如允许把子类对象赋值给父类数据类型,但不能把父类对象赋值给子类数据类型);
  • 符号引用验证 :验证类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。如果无法验证通过,则会抛出一个java.lang.IncompatibleClassChangeError 的子类异常,如 java.lang.NoSuchFieldError 、 java.lang.NoSuchMethodError 等。
3. 准备

准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段。

4. 解析

解析是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程:

  • 符号引用 :符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用 :直接引用是指可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。
    整个解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行解析。
5. 初始化

初始化阶段就是执行类构造器的 () 方法的过程,该方法具有以下特点:

  • () 方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生,编译器收集顺序由语句在源文件中出现的顺序决定。
  • () 方法与类的构造方法(即在虚拟机视角中的实例构造器 ()方法)不同,它不需要显示的调用父类的构造器,Java 虚拟机会保证在子类的 () 方法执行前,父类的 () 方法已经执行完毕。
  • 由于父类的 () 方法先执行,也就意味着父类中定义的静态语句块要优先于子类变量的赋值操作。
  • () 方法对于类或者接口不是必须的,如果一个类中没有静态语句块,也没有对变量进行赋值操作,那么编译器可以不为这个类生成 () 方法。
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。
  • Java 虚拟机必须保证一个类的 () 方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 () 方法,其他线程都需要阻塞等待。

类加载器

能够通过一个类的全限定名来获取描述该类的二进制字节流的工具称为类加载器。
每一个类加载器都拥有一个独立的类名空间,因此对于任意一个类,都必须由加载它的类加载器和这个类本身来共同确立其在 Java 虚拟机中的唯一性。
这意味着要想比较两个类是否相等,必须在同一类加载器加载的前提下;如果两个类的类加载器不同,则它们一定不相等。

双亲委派模型

从 Java 虚拟机角度而言,类加载器可以分为以下两类:

  • 启动类加载器 :启动类加载器(Bootstrap ClassLoader)由 C++ 语言实现(以 HotSpot 为例),它是虚拟机自身的一部分;
  • 其他所有类的类加载器 :由 Java 语言实现,独立存在于虚拟机外部,并且全部继承自 java.lang.ClassLoader 。
    从开发人员角度而言,类加载器可以分为以下三类:
  • 启动类加载器 (Boostrap Class Loader) :负责把存放在 \lib 目录中,或被 -Xbootclasspath 参数所指定的路径中存放的能被 Java 虚拟机识别的类库加载到虚拟机的内存中;
  • 扩展类加载器 (Extension Class Loader) :负责加载 \lib\ext 目录中,或被 java.ext.dirs 系统变量所指定的路径中的所有类库。
  • 应用程序类加载器 (Application Class Loader) :负责加载用户类路径(ClassPath)上的所有的类库。
    JDK 9 之前的 Java 应用都是由这三种类加载器相互配合来完成加载:

    上图所示的各种类加载器之间的层次关系被称为类加载器的 "双亲委派模型","双亲委派模型" 要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,需要注意的是这里的加载器之间的父子关系一般不是以继承关系来实现的,而是使用组合关系来复用父类加载器的代码。
    双亲委派模型的工作过程如下:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
    基于双亲委派模型可以保证程序中的类在各种类加载器环境中都是同一个类,否则就有可能出现一个程序中存在两个不同的 java.lang.Object 的情况。

模块化下的类加载器

JDK 9 之后为了适应模块化的发展,类加载器做了如下变化:

  • 仍维持三层类加载器和双亲委派的架构,但扩展类加载器被平台类加载器所取代;
  • 当平台及应用程序类加载器收到类加载请求时,要首先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载;
  • 启动类加载器、平台类加载器、应用程序类加载器全部继承自 java.internal.loader.BuiltinClassLoader ,BuiltinClassLoader 中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。

面试题

简述JVM类加载过程

1)加载:

  • 通过全类名获取类的二进制字节流。
  • 将类的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成类的Class对象,作为方法区数据的入口。
    2)验证:对文件格式,元数据,字节码,符号引用等验证正确性。
    3)准备:在方法区内为类变量分配内存并设置为0值。
    4)解析:将符号引用转化为直接引用。
    5)初始化:执行类构造器clinit方法,真正初始化。
简述JVM中的类加载器
  • BootstrapClassLoader启动类加载器:加载/lib下的jar包和类。 由C++编写。
  • ExtensionClassLoader扩展类加载器: /lib/ext目录下的jar包和类。由Java编写。
  • AppClassLoader应用类加载器,加载当前classPath下的jar包和类。由Java编写。
简述双亲委派机制

一个类加载器收到类加载请求之后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法完成时才尝试自己加载。
加载类顺序:BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader 检查类是否加载顺序: CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader

双亲委派机制的优点
  • 避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派保证了Java程序的稳定运行。
  • 保证核心API不被修改。
  • 如何破坏双亲委派机制
  • 重载loadClass()方法,即自定义类加载器。
如何构建自定义类加载器

新建自定义类继承自java.lang.ClassLoader,重写findClass、loadClass、defineClass方法

以上就是这个博客的全部内容了!!!

相关推荐
禹曦a9 小时前
Java实战:Spring Boot 构建电商订单管理系统RESTful API
java·开发语言·spring boot·后端·restful
code_lfh9 小时前
Spring Boot测试类的使用参考
java·spring boot·junit
superman超哥9 小时前
精确大小迭代器(ExactSizeIterator):Rust性能优化的隐藏利器
开发语言·后端·rust·编程语言·rust性能优化·精确大小迭代器
陌路209 小时前
C++28 STL容器--array
开发语言·c++
FPGAI9 小时前
Python之函数
开发语言·python
csbysj20209 小时前
NumPy Ndarray 对象
开发语言
Z1Jxxx9 小时前
删除字符串2
开发语言·c++·算法
小CC吃豆子9 小时前
Qt的信号与槽机制
开发语言·数据库·qt
你的冰西瓜10 小时前
C++中的set容器详解
开发语言·c++·stl