JVM 类的加载子系统

文章目录

  • 类的加载过程
    • 加载阶段
    • 链接阶段
    • 初始化
  • 类的加载器
    • 测试代码中获取对应的加载器
    • 获取加载器加载的路径
    • 不同类对应的加载器
    • 自定义加载器
    • 自定义加载器的方式
  • 获取类的加载器的方式
  • 双亲委派机制
    • 双亲委派机制的好处
  • Java 的 SPI 机制
    • 1. 接口定义
    • 2. 具体实现
    • 3. 配置 META-INF/services 文件
    • 4. 接口的使用

类的加载过程

在这里插入图片描述

类的加载过程分为3个阶段

  1. 加载阶段
  2. 链接阶段
  3. 初始化阶段

类的加载器,只负责加载 .class 文件,至于能不能执行,是执行引擎决定的。

加载阶段

  • 通过类的全限定名获取此类的二进制流
  • 将此类的二进制流中静态储存结构存储在运行时数据区的方法区(元空间 > 7.0 或永久代 < 7.0)
  • 在内存中生成一个 java.lang.Class 的对象,作为方法区在这个类的各种数据的访问入口

虽然一般情况下,JVM 加载的是 .class 文件,其实只要是符合 JVM 的字节码都可以进行加载。比如:.jar 包中的文件,动态代理生成的字节码(可在运行时动态生成),还比如加密的 .class 文件,通过 JVM 解密后加载,可有效防止反编译。

链接阶段

  1. 验证(Verify)
  • 每个 .class 文件都有一个特殊的开头,用以表示该文件是 JVM 支持的 .class 文件。验证的目的在于确保被加载的类的正确性。
  • 主要验证方式:文件格式验证、元数据验证、字节码验证、符号引用验证
  1. 准备(Prepare)
  • 为类变量分配内存并且设置类变量的初始值(注意:初始值为 0)。如:private static int a = 10;这个时候 a 的值为 0;特殊的:如果变量用 final 修饰,如:private final static int b = 10;这个时候 b 的值为 10;因为 final 修饰表示 b 为常量,在编译期初始化。
  • 此时成员变量不会初始化,static 修饰的 a 为类变量,类变量在类加载的时候初始化,如 private int c = 10;c 是成员变量,是和对象一起被分配到 Java 堆中的。
  1. 解析(Resolve)
  • 将常量池内的符号引用转换为直接引用的过程
  • 解析主要针对类或接口、字段、各种方法(类方法、接口方法等)

初始化

  • 初始化静态变量和静态块,此对应执行类构造器方法<clinit>(),此方法的指令按源文件中的顺序执行。如果没有相关静态变量或静态块,可能不会有 <clinit>() 方法。
  • 成员变量和局部变量对应 JVM 下的 <init>()方法。

类的加载器

  • 引导类(启动类)加载器(Bootstrap ClassLoder):负责加载 Java 的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar、sun、java、javax 包开头的类 ),使用 C/C++ 实现的,并不继承自 ClassLoader 类。
  • 自定义类加载器:直接或间接继承自 ClassLoader 类的类加载器。是由 Java 实现的。
    • 扩展类加载器(sun.misc.Launcher$ExtClassLoader):ExtClassLoader 是 Launcher 对象的内部类,其父类加载器为引导类加载器,其加载的是系统属性 java.ext.dirs 所指定的目录 或者 从 JAVA_HOME/jre/lib/ext 目录下加载类。(我们自定义的 jar 包放在此目录也会被加载)
    • 系统类(应用程序类)加载器(sun.misc.Launcher$AppClassLoader):其父类加载器为扩展类加载器,负责加载环境变量 classpath 或 系统属性 java.class.path 所指定的目录下的类。
    • 其他自定义的类加载器
      在这里插入图片描述

注意,他们不是继承关系,我们可以称他们为扩展关系。

测试代码中获取对应的加载器

    public static void main(String[] args) {
        ClassLoader app = ClassLoader.getSystemClassLoader();
        System.out.println(app);

        ClassLoader ext = app.getParent();
        System.out.println(ext);

        ClassLoader bootstrap = ext.getParent();
        System.out.println(bootstrap);
    }

结果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@6d03e736
null

程序中的默认类加载器为 AppClassLoader 。一般情况下,Java 应用中的类都是由 AppClassLoader 加载器加载。其通过 ClassLoader.getSystemClassLoader() 方法获取。

Java 代码中不能直接获取引导类加载器实例。所以示例中 bootstrap 为 null

获取加载器加载的路径

        // 获取 Bootstrap 加载器的加载路径
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for(URL url : urls){
            System.out.println(url.getFile());
        }

        System.out.println("======================");
        // 获取扩展类加载器加载的的路径
        String dirs = System.getProperty("java.ext.dirs");
        System.out.println(dirs);

结果

/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/resources.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/rt.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/sunrsasign.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/jsse.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/jce.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/charsets.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/jfr.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/classes
======================
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

Bootstrap 加载器的加载路径下的 jar 包文件包含的类,由 Bootstrap 加载器加载,两个 ext 目录下的 jar 包下的类由 ExtClassLoader 加载器加强,我们的应用程序,classpath 属性下的类,由 AppClassLoader 加载。

不同类对应的加载器

        ClassLoader classLoader = String.class.getClassLoader();
        System.out.println(classLoader);

        ClassLoader classLoader1 = ExecutorTest.class.getClassLoader();
        System.out.println(classLoader1);

结果

null
sun.misc.Launcher$AppClassLoader@18b4aac2

说明 String 类是由引导类加载器加载,其引导类加载器无法在代码中获取,所以为 null 。ExecutorTest 是我们的示例对象,其由 AppClassLoader 加载器加载。当然,我们在 ext 目录下找到的 jar 包中的类,由 ExtClassLoader 加载器加载。

自定义加载器

一般的 Java 程序中,使用引导类加载器、扩展类加载器、系统类加载器相互作用,即可。几乎不需要自定义类的加载器,我们可以在某些情况下进行自定义加载器。

  • 修改类的加载方式
  • 还比如我们对编译的源码进行了加密,在类加载时需要解密等

自定义加载器的方式

  1. 继承 ClassLoader 重写 findClass(String name) 方法
  2. 按照 URLClassLoader.FactoryURLClassLoader 继承 URLClassLoader 的方式继承 URLClassLoader 来实现即可。( URLClassLoader 可以加载 jar 包下的类)
package com.yyoo.jvm;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

@Getter
@Setter
@AllArgsConstructor
public class MyClassLoader extends ClassLoader{

    /**
     * 当前加载器的 class 文件根路径
     */
    private String classRootPath;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        // 使用 io 流读取.class 字节码文件,然后使用父类的 defineClass 方法返回为 Class 类
        try (InputStream in = new FileInputStream(getClassRootPath()+"\\"+name.replaceAll(".","\\")+".class")){
            byte[] b = new byte[1024];
            int len = in.read(b);
            return defineClass(name,b,0,len);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException();

    }

}

name 参数使用class的全类名即可。注:我们示例使用了 lombok 插件。

获取类的加载器的方式

// 获取当前类的ClassLoader
Object o = new Object();
System.out.println(o.getClass().getClassLoader());

// 获取当前线程上下文的ClassLoder
System.out.println(Thread.currentThread().getContextClassLoader());

// 获取当前系统的 ClassLoder
System.out.println(ClassLoader.getSystemClassLoader());

双亲委派机制

在这里插入图片描述
类的加载过程:

  1. 类的加载器收到类的加载请求时,它不会立马去加载,而且把加载请求委托给父类加载器去执行。
  2. 如果父类加载器还有父类加载器,则会进一步委托给父类加载器加载,直到最顶层的类加载器。
  3. 如果父类加载器可以完成类的加载,则由父类加载器加载该类,如果父类加载器无法加载,子加载器才会尝试自己加载

以上整个过程称为双亲委派机制。

示例1

    public static void main(String[] args) throws ClassNotFoundException {

        String classRootPath = "D:\\work\\code\\mytest\\peixun\\target\\classes";
        // 同一个 ClassLoader 实例,加载同一个 Class ,得到的是同一个 Class 对象
        ClassLoader my = new MyClassLoader(classRootPath);
        Class a = my.loadClass("com.yyoo.jvm.MyEmp");
        Class b = my.loadClass("com.yyoo.jvm.MyEmp");

        System.out.println(a == b);// true

        // 同一个 ClassLoader 的不同实例,加载同一个 Class 文件,得到的也是同一个 Class 对象
        ClassLoader my1 = new MyClassLoader(classRootPath);
        Class c = my1.loadClass("com.yyoo.jvm.MyEmp");
        System.out.println(a == c);// true

        // 不同的 ClassLoader ,加载同一个 Class 文件,得到的也是同一个 Class 对象
        ClassLoader app = ClassLoader.getSystemClassLoader();
        Class d = app.loadClass("com.yyoo.jvm.MyEmp");
        System.out.println(a == d);// true


        System.out.println(a.getClassLoader());// 系统类加载器
        System.out.println(d.getClassLoader());// 系统类加载器

    }

注:MyClassLoader 即为我们上面自定义的 ClassLoader。

根据双亲委派机制来解释该现象,我们自定义加载器的 classRootPath 其实就是我们应用的 classPath,而 classPath 下的类是由系统类加载器加载的,而且其加载的始终是 classPath 下的类,而我们的 ClassRootPath 下的类永远不会加载(除非我们自定义加载的类和系统类加载器加载的类全类名有不同的地方 或者 classPath 下没有该类)。

示例2

示例1的前提是,MyEmp 的 .class 文件存在于我们应用的 classPath 路径下,示例 2 ,我们将 classPath 路径下的 .class 文件删除,并按包路径创建文件夹在 D 盘根路径下(Java 的几大类加载器加载的路径和目录之外),再次执行

String classRootPath = "D:";
// 同一个 ClassLoader 实例,加载同一个 Class ,得到的是同一个 Class 对象
ClassLoader my = new MyClassLoader(classRootPath);
Class a = my.loadClass("com.yyoo.jvm.MyEmp");
Class b = my.loadClass("com.yyoo.jvm.MyEmp");

System.out.println(a == b);

// 同一个 ClassLoader 的不同实例,加载同一个 Class 文件,得到的不是同一个 Class 对象
ClassLoader my1 = new MyClassLoader(classRootPath);
Class c = my1.loadClass("com.yyoo.jvm.MyEmp");
System.out.println(a == c);


System.out.println(a.getClassLoader());
System.out.println(c.getClassLoader());

结果:

true
false
com.yyoo.jvm.MyClassLoader@3f99bd52
com.yyoo.jvm.MyClassLoader@3a71f4dd

可以看到在 系统类加载器、扩展类加载器、启动类加载器加载的路径之外的 .class 文件会使用我们自定义的加载器加载,且不同的 MyClassLoader 实例加载的 Class 对象不是同一个。

双亲委派机制的好处

  • 避免类的重复加载
  • 保护程序安全,防止核心 API (Java 核心类)被随意篡改

Java 的 SPI 机制

SPI(service provider interface)是 jdk 内置的一种服务提供发现机制。可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,常用的关系型数据有 Mysql、Oracle、SQLServer、DB2 等,这些不同类型的数据库使用的驱动程序各不相同,那 JDK 不可能把所有厂商的驱动都实现,只能制定一个标准接口,其他不同厂商可以针对同一接口做出不同的实现,各个数据库厂商根据标准来实现自己的驱动,这就是SPI机制。

通俗点来说,SPI 就是某个应用程序只提供接口规则,具体的实现需要调用方(通常该接口会有多个实现,否则也就用不着 SPI 了)在使用时自行实现,其实现方式遵循 Java 的 SPI 机制。

比如:我们要提供一个文件上传的接口,其实现有如下几种方式:服务器本地存储、上传到 ftp 服务器、上传到 minio、上传到云服务等等

1. 接口定义

public interface FileUpload {
    /**
     * 上传文件
     */
    void upload();
    
}

2. 具体实现

public class LocalFileUpload implements FileUpload{

    @Override
    public void upload() {
        System.out.println("将文件存储到服务器本地路径下");
    }
}

public class MinIOFileUpload implements FileUpload{
    @Override
    public void upload() {
        System.out.println("将文件上传到 MinIOn 服务器");
    }
}

3. 配置 META-INF/services 文件

在应用的 META-INF/services/ 目录下(目录不存在自行创建即可),创建文件名称为 com.yyoo.spi.FileUpload 的文本文件

在这里插入图片描述
文件内容为:

com.yyoo.spi.LocalFileUpload
com.yyoo.spi.MinIOFileUpload

4. 接口的使用

// load 方法,如果不传 ClassLoader 则默认使用当前线程上下文的 Thread.currentThread().getContextClassLoader() 
// 这里即是系统类加载器,我们也可以使用重载方法 load(Class<S> service,ClassLoader loader) 来指定加载器
ServiceLoader<FileUpload> serviceLoader = ServiceLoader.load(FileUpload.class);

// 获取迭代器或者直接使用 foreach 语句即可获取 META-INF/services/com.yyoo.spi.FileUpload 文件中配置的所有实现
for (FileUpload fileUpload : serviceLoader){
    System.out.println(fileUpload.getClass());
    System.out.println(fileUpload.getClass().getClassLoader());// 使用系统类加载器加载
    fileUpload.upload();
}

执行结果:

class com.yyoo.spi.LocalFileUpload
sun.misc.Launcher$AppClassLoader@18b4aac2
将文件存储到服务器本地路径下
class com.yyoo.spi.MinIOFileUpload
sun.misc.Launcher$AppClassLoader@18b4aac2
将文件上传到 MinIOn 服务器

ServiceLoader 是 java.util 包提供的工具类,其主要作用就是通过读取 META-INF/services 下的配置,然后根据配置通过系统类加载器加载对应配置的实现类。

SPI 允许应用程序外部提供内部接口的实现,从而改变了内部接口的行为,在某种程度上规避了双亲委派机制思想(核心 API 实现被改变)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/106457.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

StreamSaver.js入门教程:优雅解决前端下载文件的难题

本文简介 点赞 关注 收藏 学会了 本文介绍一个能让前端优雅下载大文件的工具&#xff1a;StreamSaver.js ⚡️ StreamSaver.js GitHub地址⚡️ 官方案例 StreamSaver.js 可用于实现在Web浏览器中直接将大文件流式传输到用户设备的功能。 传统的下载方式可能导致大文件的加…

centos ubantu IP一直变化,远程连接不上问题

文章目录 一、为什么IP地址会变1.主机DHCP导致 二、解决IP地址变化1.centos2.ubantu 总结 虚拟机能连接为互联网,但下一次启动IP地址再发生变化,无法使用ssh远程连接 一、为什么IP地址会变 1.主机DHCP导致 虚拟机系统(ubantu,centos…)启动后会向本地申请IP地址租约,租聘的I…

Java采集传感器数据,亲测有效!

背景 先说背景&#xff0c; 最近公司项目需要用到传感器&#xff0c;采集设备温湿度&#xff0c;倾斜角&#xff0c;电流…&#xff0c;公司采购采购了一个温湿度传感器给我们开发测试使用&#xff0c;如下图&#xff1a; 看着还挺精致有没有。 进入正题 有了这个温湿度传感器…

基于ChatGPT实现AI软件研发 | 神经网络 | 数据

计算机技术的发展和互联网的普及&#xff0c;使信息处理和传输变得更加高效&#xff0c;极大地改变了金融、商业、教育、娱乐等领域的运作方式。数据分析、人工智能和云计算等新兴技术&#xff0c;也在不断地影响和改变着各个行业。 如今&#xff0c;我们正在见证人工智能技术…

Redis(04)| 数据结构-压缩列表

压缩列表的最大特点&#xff0c;就是它被设计成一种内存紧凑型的数据结构&#xff0c;占用一块连续的内存空间&#xff0c;不仅可以利用 CPU 缓存&#xff0c;而且会针对不同长度的数据&#xff0c;进行相应编码&#xff0c;这种方法可以有效地节省内存开销。 但是&#xff0c;…

RK3568-pcie接口

pcie接口与sata接口 pcie总线pcie总线pcie控制器sata控制器nvme设备sata设备nvme协议ahci协议m-key接口b-key接口RC模式和EP模式 RC和EP分别对应主模式和从模式,普通的PCI RC主模式可以用于连接PCI-E以太网芯片或PCI-E的硬盘等外设。 RC模式使用外设一般都有LINUX驱动程序,安…

计算机操作系统重点概念整理-第四章 内存管理【期末复习|考研复习】

第四章 内存管理 【期末复习|考研复习】 计算机操作系统系列文章传送门&#xff1a; 第一章 计算机系统概述 第二章 进程管理 第三章 进程同步 第四章 内存管理 第五章 文件管理 第六章 输出输出I/O管理 文章目录 第四章 内存管理 【期末复习|考研复习】前言四、内存管理4.1 内…

企业安全—DevSecOps概述详情

0x00 前言 SDL存在的问题在于体量过于庞大&#xff0c;不利于快速进行适配和进行&#xff0c;所以就有了DevSecOps&#xff0c;实际上是因为敏捷开发也就是DevOps的推进&#xff0c;并且坐上了云服务模式的火车&#xff0c;所以这一系列的东西都开始普及。DevSecOps作为DevOps…

PTE-写作 学习(一)

目录 PTE写作 写作技能 词汇积累 熟悉机经 pte写作考的就是态度 写作技能 看一段写一句 蓝色框里的单词是不可以使用的 &#xff0c;他们是副词&#xff0c;要添加新的句子 PTE写作 写作技能 词汇积累 熟悉机经 题库太窄 pte写作考的就是态度 写作技能 极有模板可…

【数据结构】选择排序

⭐ 作者&#xff1a;小胡_不糊涂 &#x1f331; 作者主页&#xff1a;小胡_不糊涂的个人主页 &#x1f4c0; 收录专栏&#xff1a;浅谈数据结构 &#x1f496; 持续更文&#xff0c;关注博主少走弯路&#xff0c;谢谢大家支持 &#x1f496; 直接选择、堆排序 1. 直接选择排序2…

泛微OA之获取每月固定日期

文章目录 1.需求及效果1.1需求1.2效果 2. 思路3. 实现 1.需求及效果 1.1需求 需要获取每个月的7号作为需发布日期&#xff0c;需要自动填充1.2效果 自动获取每个月的七号2. 思路 1.功能并不复杂&#xff0c;可以用泛微前端自带的插入代码块的功能来实现。 2.将这需要赋值的…

Redis -- 基础知识2

1.Redis客户端介绍 1.基础介绍 Redis是一种客户端-服务器结构的程序&#xff0c;通过网络进行互动 客户端的多种形态 1.自带了命令行客户端&#xff1a;redis-cil 2.图形化界面的客户端&#xff1a;依赖windows系统&#xff0c;连接服务器有诸多限制&#xff0c;不建议使用 3.基…

模拟算法及其优化

第一题 替换所有问号 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 class Solution { public:string modifyString(string s) {string ret;for(int i0;i<s.size();i){if(i0){if(s[i]?&&i1<s.size()){for(char aa;a<z;a){if(a!s…

J2EE项目部署与发布(Windows版本)

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《Spring与Mybatis集成整合》《Vue.js使用》 ⛺️ 越努力 &#xff0c;越幸运。 1.单机项目的部署 1.1们需要将要进行部署的项目共享到虚拟机中 在部署项目之前&#xff0c;我们先要检查一下…

基于情感词典的情感分析方法

计算用户情绪强弱性&#xff0c;对于每一个文本都可以得到一个情感分值&#xff0c;以情感分值的正负性表示情感极性&#xff0c;大于0为积极情绪&#xff0c;小于0反之&#xff0c;绝对值越大情绪越强烈。 基于情感词典的情感分析方法主要思路&#xff1a; 1、对文本进行分词…

数据结构与算法基础(青岛大学-王卓)(9)

终于迎来了最后一部分(排序)了&#xff0c;整个王卓老师的数据结构就算是一刷完成了&#xff0c;但是也才是数据结构的开始而已&#xff0c;以后继续与诸位共勉 &#x1f603; (PS.记得继续守护家人们的健康当然还有你自己的)。用三根美味的烤香肠开始吧。。。 文章目录 排序基…

CSS 基础知识-02

CSS 基础知识-01 1. flex布局2.定位3.CSS精灵4.CSS修饰属性 1. flex布局 2.定位 3.CSS精灵 4.CSS修饰属性

NEWCC:新时代的区块链生态新币私募造势平台

在当今区块链领域&#xff0c;这项技术已经为金融资产注入了全新的生机&#xff0c;同时也为初创企业提供了新的商业模式和融资机会。通过代币的金融属性&#xff0c;企业和项目方得以实现资本的初期积累&#xff0c;同时在区块链空间以更低成本和更高效率进行交易和服务创新。…

【Oracle】[INS-30131]执行安装程序验证所需的初始设置失败。

这里写目录标题 一、问题描述1 报错内容1.1 无法从节点“kotin”检索 exectask 的版本1.2 工作目录"xxx"无法在节点"kotin"上使用 2 相关环境2.1 安装软件2.2 安装系统 3 解决思路分析 二、解决方案1 方案一、 满足验证条件 - 不换系统1.1 第一步、检查文件…

如何 通过使用优先级提示,来控制所有网页资源加载顺序

当你打开浏览器的网络标签时&#xff0c;你会看到大量的活动。资源正在下载&#xff0c;信息正在提交&#xff0c;事件正在记录&#xff0c;等等。 由于有太多的活动&#xff0c;有效地管理这些流量的优先级变得至关重要。带宽争用是真实存在的&#xff0c;当所有请求同时触发时…