JVM体系-类加载

本文介绍了JVM的类加载过程,包括加载、验证、准备、初始化和卸载五个阶段。详细讨论了双亲委派模型及其存在的问题,即父加载器无法识别子加载器的资源。为解决此问题,提出了线程上下文类加载器(Thread Context ClassLoader)的概念,允许通过setContextClassLoader()方法设置并获取子加载器资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类加载

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

  2. 类加载的时机:加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,必须按这个顺序来,而解析阶段不一定,可以在初始化之后开始。

    以下6中情况需要立刻初始化:1.使用new关键字实例化对象的时候、读取或设置一个类型的静态字段、调用一个类型的静态方法的时候,
    	即遇到new、getstatic、putstatic或invokestatic这四条字节码指令。
    2.使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。
    3.当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类
    5.当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
    6.当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、
    	REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
    
  3. 类加载的过程

     1. 加载。加载阶段虚拟机需要做的三件事:
      				1. 通过一个类的全限定名来获取定义此类的二进制字节流。
      				2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
      				3. 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
     		如果是非数组类型的加载阶段。可以通过自定义的类加载器或者虚拟机内置的类加载器来加载。
     		如果是数组类型,数组类本身不通过类加载器创建,它是由Java虚拟机直接在 内存中动态构造出来的。
     			但是数组中的元素类型如果是引用类型,那么还是可用自定义或者内置加载器。(一个类型必须与类加载器一起确定唯一性)
     						如果不是引用类型,把数组 标记为与引导类加载器关联。
     2. 验证。确保Class文件的字节流中包含的信息符合规范,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
     				1. 文件格式验证。验证字节流是否符合Class文件格式的规范,能否被虚拟机处理。
     						  比如:是否以魔数0xCAFEBABE开头。
     							 主、次版本号是否在当前Java虚拟机接受范围之内。
     						 	常量池的常量中是否有不被支持的常量类型
     				2. 元数据验证。对类的元数据信息进行验证,保证描述的信息符合规范。验证比如:这个类是否有父类、这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
     				3. 字节码验证。通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。本阶段属于对类的方法体进行校验分析,
     							保证被校验类的方法在运行时不会做出危害 虚拟机安全的行为。
     				4. 符号引用验证。发生在虚拟机将符号引用转化为直接引用的时候,对类自身以外(常量池中的各种符号 引用)的各类信息进行匹配性校验,
     							即该类是否缺少或者被禁止访问它依赖的某些外部 类、方法、字段等资源。
     							目的:确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机 将会抛出异常。
     3. 准备。为类中静态变量分配内存并设置类变量初 始值的阶段。这时候进行内存分配的 仅包括类变量,而不包括实例变量。
     				例如:public static  int a=100;则这个阶段a=0;
     4. 解析。将常量池内的符号引用替换为直接引用。
     				什么是符号引用?:符号引用以一组符号来描述所引用的目标,符号可以是任何 形式的字面量,只要使用时能无歧义地定位到目标即可。
     				什么是直接引用?:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄。
     				
     			!!虚拟机对同一个符号引用可以进行多次解析,除了invokedynamic指令,虚拟机实现可 以对第一次解析的结果进行缓存,从而避免解析动作重复进行。
     			如果符号引用之前已经被成功解析,那么后续的引用解析请求就应当一直能够成功;
     			如果第一次解析失败了,其他指令对这个符号的解析请求也应该收到相同的异常,哪 怕这个请求的符号在后来已成功加载进Java虚拟机内存之中。
     			invokedynamic指令用来支持动态语言,必须等到程序实际执行这条指令解析动作才能成功。
     5. 初始化。在准备阶段,变量已经赋过一次系统要求的初始零值,初始化阶段需要将这些变量赋值为程序员设定的内容。
     				即初始化阶段就是执行类构造器<clinit>()方法的过程。
     			需要注意的几个点:1.<clinit>()方法与类的构造函数(即实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行 完毕。
     								2.由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
     								3.虽然接口中不能使用静态语句块,但仍然会生成<clinit>()方法。
     								4.如果多个线程同时初始化一个类,则只有一个线程去执行<clinit>()方法。
    
  4. 类加载器

     	1.	BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib ⽬录下的jar包和类或者或															
     										被 -Xbootclasspath 参数指定的路径中的所有类
     	2.  ExtensionClassLoader(扩展类加载器) :主要负责加载⽬录 %JRE_HOME%/lib/ext ⽬录下的jar包和类,或被 java.ext.dirs 系统
     										变量所指定的路径下的jar包。
     	3.  AppClassLoader(应⽤程序类加载器) :⾯向我们⽤户的加载器,负责加载当前应⽤classpath下的所有jar包和类。
     	4. 我们也可以自定义类加载器,继承ClassLoader,重载loadClass() 。
    
  5. 双亲委派模型
    在这里插入图片描述

     	1. 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承的关系来实现的,
     		而是通常使用 组合关系来复用父加载器的代码。
     	2. 双亲委派模型工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,
     		每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求
     		(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
     	3. 双亲委派模型的优点:
     					1. Java类随着它的类加载器一起,具备了带有优先级的层次关系。
     					2. 避免类的重复加载,保证一种类都由同一个类加载器加载。不会出现多个不同的Object类。
    
  6. 破坏双亲委派模型
    https://www.cnblogs.com/fengtingxin/p/11872710.html
    双亲委派模型存在的问题:父加载器无法向下识别子加载器的资源。
    如何解决?:引入了线程上下文类加载器(Thread Context ClassLoader),可以通过类的setContextClassLoader()方法
    进行设置。再通过getContextClassLoader()来获取子加载器资源

### JVM 类加载机制原理 JVM类加载机制是一种动态加载的方式,它允许 Java 应用程序在运行时按需加载所需的类文件。这种机制的核心部分由 **双亲委派模型** **类加载器层次结构** 组成。 #### 双亲委派模型的工作方式 当一个类加载器收到类加载请求时,它不会立即尝试自己去加载该类,而是先把这个请求委托给它的父类加载器处理。这一过程会逐级向上直到到达顶层的启动类加载器(Bootstrap ClassLoader),只有当父类加载器无法完成加载任务时,当前类加载器才会尝试自行加载目标类[^1]。 #### 类加载器的层次结构 JVM 中存在三种主要类型的类加载器以及一种可选的自定义类加载器: - **启动类加载器 (Bootstrap ClassLoader)** 负责加载位于 `JAVA_HOME/jre/lib` 下的核心类库,例如 `java.lang.*` 系列包中的类[^4]。 - **扩展类加载器 (Extension ClassLoader)** 加载存放在 `JAVA_HOME/jre/lib/ext` 或者通过 `-D java.ext.dirs` 参数指定位置上的 JAR 文件中的类。 - **应用程序类加载器 (Application ClassLoader)** 这是最常见的类加载器之一,默认用于加载用户的应用程序代码所在的路径下(即 CLASSPATH 所指明的位置)的所有类。 - **自定义类加载器** 开发人员可以通过继承 `ClassLoader` 来创建自己的类加载器以满足特定需求。如果不希望破坏现有的双亲委派模式,则只需覆盖其子方法 `findClass()` 即可;而要改变默认行为并绕过此规则,则需要重新编写 `loadClass()` 方法来实现新的逻辑[^3]。 #### 类加载的过程阶段划分 整个类加载可以分为以下几个重要阶段: 1. **加载**: 将字节码读入内存,并将其转换为对应的二进制数据流形式存储于方法区中; 2. **验证**: 对输入的数据进行校验,确保它们符合虚拟机规范的要求,防止潜在的安全隐患; 3. **准备**: 创建静态变量并将这些字段初始化为其所属类型规定的初始值; 4. **解析**: 把常量池内的符号引用替换成为直接引用; 5. **初始化**: 设置类的静态变量到正确的初始值,并执行任何必要的静态语句块操作。 以上就是关于 JVM 如何利用双亲委派模型及其背后的分类体系来进行有效的类管理的一个概述说明。 ```python class CustomClassLoader(ClassLoader): def findClass(self, name): # 自定义查找逻辑 byte_array = self.loadByteCode(name) return super().defineClass(byte_array) def loadByteCode(self, className): with open(f"{className}.class", 'rb') as f: return bytearray(f.read()) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值