当前位置: 首页 > news >正文

Java 入门指南:JVM(Java虚拟机)—— Java 类加载器详解

类加载器

类加载器(Class Loader)是 Java 虚拟机(JVM)的一部分,它的作用是将类的字节码文件(.class 文件)从磁盘或其他来源加载到 JVM 中。类加载器负责查找和加载类的字节码文件,并将其转化为 Class 对象。

类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。

根据官方 API 文档的介绍:

类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。

每个 Java 类都有一个引用指向加载它的 ClassLoader。但数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。

  • 每个 Java 类都有一个引用指向加载它的 ClassLoader

  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。

组成部分

在 Java 中,类加载器主要有三个层次:

  1. 启动类加载器(Bootstrap ClassLoader):这是最基础的类加载器,由 C++ 实现,通常表示为 null,并且没有父级,负责加载扩展目录下的 jar 包和系统类路径下的核心库( %JAVA_HOME%/lib 目录下的 rt.jarresources.jarcharsets.jar 等 jar 包和类)以及被 -Xbootclasspath 参数指定的路径下的所有类。

    rt.jar:rt 代表“RunTime”,rt.jar 是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*java.io.*java.nio.*java.lang.*java.sql.*java.math.*

  2. 扩展类加载器(Extension ClassLoader):由 Java 实现,负责加载 Java 默认扩展目录下的 jar 包(%JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类)。

  3. 系统类加载器(System/App ClassLoader):也称为应用程序类加载器,由 Java 实现,负责加载用户类路径(classpath)下的所有 jar 包和类。

![[Pasted image 20240915225845.png]]

除了这三个内置的类加载器外,还可以自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现,以满足特殊的需求。例如,可以通过自定义类加载器来加载网络上的类,或者从数据库中加载类。

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。

ClassLoader

除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader 抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

每个 ClassLoader 可以通过 getParent() 获取其父 ClassLoader,如果获取到 ClassLoadernull 的话,那么该类是通过 BootstrapClassLoader 加载的。由于 BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

public abstract class ClassLoader {...// 父加载器private final ClassLoader parent;@CallerSensitivepublic final ClassLoader getParent() {//...}...
}

下面是一个获取 ClassLoader 的示例:

public class PrintClassLoaderTree {public static void main(String[] args) {ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();StringBuilder split = new StringBuilder("|--");boolean needContinue = true;while (needContinue){System.out.println(split.toString() + classLoader);if(classLoader == null){needContinue = false;}else{classLoader = classLoader.getParent();split.insert(0, "\t");}}}}

输出结果:

|--sun.misc.Launcher$AppClassLoader@18b4aac2|--sun.misc.Launcher$ExtClassLoader@53bd815b|--null

可以看出:

  • 自定义编写的 Java 类 PrintClassLoaderTreeClassLoaderAppClassLoader
  • AppClassLoader 的父 ClassLoaderExtClassLoader
  • ExtClassLoader 的父 ClassLoaderBootstrap ClassLoader,因此输出结果为 null。

自定义类加载器

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader抽象类。

ClassLoader 类有两个关键的方法:

  • protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。

  • protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。

官方 API 文档中写到:

建议 ClassLoader的子类重写 findClass(String name)方法而不是loadClass(String name, boolean resolve) 方法。

如果我们不想打破双亲委派模型,就需要重写 ClassLoader 类中的 findClass() 方法,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

实现自定义类加载器

以下是我们自行实现自定义类加载器的一个示例:

import java.io.*;public class CustomClassLoader extends ClassLoader {private String pathToBin;public CustomClassLoader(String pathToBin) {this.pathToBin = pathToBin;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] classData = loadClassData(name);return defineClass(name, classData, 0, classData.length);} catch (IOException e) {throw new ClassNotFoundException("Class " + name + " not found", e);}}private byte[] loadClassData(String name) throws IOException {String file = pathToBin + name.replace('.', File.separatorChar) + ".class";InputStream is = new FileInputStream(file);ByteArrayOutputStream byteSt = new ByteArrayOutputStream();int len = 0;while ((len = is.read()) != -1) {byteSt.write(len);}return byteSt.toByteArray();}
}

示例说明:

  • 构造器:接受一个字符串参数,这个字符串指定了类文件的存放路径。
  • 覆写 findClass 方法:当父类加载器无法加载类时,findClass 方法会被调用。在这个方法中,首先使用 loadClassData 方法读取类文件的字节码,然后调用 defineClass 方法来将这些字节码转换为 Class 对象。
  • loadClassData 方法:读取指定路径下的类文件内容,并将内容作为字节数组返回。

类加载器加载规则

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

public abstract class ClassLoader {...private final ClassLoader parent;// 由这个类加载器加载的类。private final Vector<Class<?>> classes = new Vector<>();// 由 JVM 调用,用此类加载器记录每个已加载类。void addClass(Class<?> c) {classes.addElement(c);}...
}

类加载器工作过程

类加载器(Class Loader)在 Java 虚拟机(JVM)中的工作过程是一个复杂而精细的流程。类加载器不仅负责加载类的字节码文件,还要确保类的正确性和初始化。

JVM(Java虚拟机)——类的生命周期与加载过程

类加载器的工作过程可以分为以下几个主要阶段:

  1. 加载(Loading):在加载阶段,类加载器负责读取类的二进制数据,并将其转化为 Class 对象。这一阶段包括以下几个步骤:
  • 查找或获取类的二进制数据:类加载器会根据类的全限定名(例如 com.example.MyClass)查找并加载类的字节码文件。
  • 生成 Class 对象:类加载器将字节码文件转化为 Class 对象,并存放在方法区中。
  1. 验证(Verification):验证阶段是为了确保类文件的字节码符合 Java 虚拟机的规范,防止恶意代码危害虚拟机。验证阶段主要包括以下几个子阶段:

    • 文件格式验证:确保字节流的格式符合 Class 文件格式规范。
    • 元数据验证:确保类的元数据信息(如常量池中的常量)正确无误。
    • 字节码验证:确保字节码指令符合 JVM 规范,不会导致非法操作。
    • 符号引用验证:确保符号引用能正确解析到实际存在的类、接口、方法或字段。
  2. 准备(Preparation):准备阶段主要是为类变量分配内存空间,并设置类变量的初始值。注意,这里的类变量指的是被 static 修饰的变量。实例变量则是在对象实例化时分配内存空间。

  3. 解析(Resolution):解析阶段是将符号引用转换为直接引用的过程。符号引用指的是类名、接口名、方法名等字符串形式的引用,而直接引用则指向目标对象在内存中的地址。

  4. 初始化(Initialization):初始化阶段是执行类构造器 (<clinit>) 方法的过程。在这个阶段,类中的静态变量会被赋予初始值,并执行静态块中的代码。初始化阶段还包括类中非静态方法的调用和类的实例化。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【iOS】单例模式
  • 基于python+django+vue的图书管理系统
  • 传输层协议 —— TCP协议(上篇)
  • 学习Java(一)类和对象
  • 安卓开发,如何实现apk的代码混淆、日志混淆?
  • 音视频入门基础:AAC专题(10)——FFmpeg源码中计算AAC裸流每个packet的pts、dts、pts_time、dts_time的实现
  • 【d46】【Java】【力扣】234.回文链表
  • 详解QT元对象系统用法
  • RK3568部署DOCKER启动服务器失败解决办法
  • 实用小工具——多标签页插件Office Tab介绍
  • C++ 解析 RDP 协议
  • 分布式Redis(14)哈希槽
  • 数据可视化pyecharts——数据分析(柱状图、折线图、饼图)
  • 【算法——双指针】
  • 每日一题——第九十七题
  • [Vue CLI 3] 配置解析之 css.extract
  • 《深入 React 技术栈》
  • 【399天】跃迁之路——程序员高效学习方法论探索系列(实验阶段156-2018.03.11)...
  • 【vuex入门系列02】mutation接收单个参数和多个参数
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • angular组件开发
  • create-react-app做的留言板
  • export和import的用法总结
  • java2019面试题北京
  • JavaScript标准库系列——Math对象和Date对象(二)
  • Mysql优化
  • SpiderData 2019年2月25日 DApp数据排行榜
  • vue-cli3搭建项目
  • 简单基于spring的redis配置(单机和集群模式)
  • “十年磨一剑”--有赞的HBase平台实践和应用之路 ...
  • 函数计算新功能-----支持C#函数
  • ​VRRP 虚拟路由冗余协议(华为)
  • #LLM入门|Prompt#3.3_存储_Memory
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • (¥1011)-(一千零一拾一元整)输出
  • (done) 两个矩阵 “相似” 是什么意思?
  • (SERIES10)DM逻辑备份还原
  • (八)五种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (第27天)Oracle 数据泵转换分区表
  • (附源码)ssm高校志愿者服务系统 毕业设计 011648
  • (考研湖科大教书匠计算机网络)第一章概述-第五节1:计算机网络体系结构之分层思想和举例
  • (论文阅读32/100)Flowing convnets for human pose estimation in videos
  • (南京观海微电子)——I3C协议介绍
  • (三分钟了解debug)SLAM研究方向-Debug总结
  • (转)Groupon前传:从10个月的失败作品修改,1个月找到成功
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • (转)Oracle 9i 数据库设计指引全集(1)
  • .babyk勒索病毒解析:恶意更新如何威胁您的数据安全
  • .Net Core 中间件验签
  • .NET MAUI学习笔记——2.构建第一个程序_初级篇
  • .net 连接达梦数据库开发环境部署
  • .NET 事件模型教程(二)
  • .NET下的多线程编程—1-线程机制概述
  • .Net中间语言BeforeFieldInit
  • .NET中使用Redis (二)