【JVM】一文详解类加载器

devtools/2024/10/17 15:47:47/

文章目录

  • 类加载器的概述
  • 类加载器的分类
    • 启动类加载器(`Bootstrap ClassLoader `)
    • 扩展类型加载器(ExClassLoader)
    • 系统类加载器(Application ClassLoader )
    • 总结
  • 双亲委派机制
    • 概念
    • 双亲委派机制的优势
  • ClassLoader
      • `findClass`
      • `defineClass`
      • loadClass,findClass,defineClass之间的关系
      • resolveClass
  • URLClassLoader
      • 案例一:加载磁盘上的类
        • 解释一下`URI`和`URL`
      • 自定义类加载器
  • 类的显式和隐式加载
    • 显式加载
    • 隐式加载

类加载器的概述

Java中的类加载机制是指在Java程序运行时,将类文件(通常是.class文件)加载到内存中的一系列步骤和过程。这一机制确保了类能够在需要的时候被正确、安全地加载到Java虚拟机(JVM)中,并进行初始化和使用。Java的类加载机制遵循着“按需加载”原则,即只有在需要用到某个类的时候,才会加载该类。
在这里插入图片描述
简单来说:ClassLoaderJava的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例
在这里插入图片描述

类加载器的分类

启动类加载器(Bootstrap ClassLoader )

根类加载器,是虚拟机的一部分,由C++语言实现的(所以不属于Java当中的某个具体的类,打印的时候会显示null) ,且没有父加载器,没有继承java.lang.ClassLoader .主要用于加载JDK核心库,加载时的搜索路径为sun.boot.class.path(JDK8之前) java.class.path (JDK17) .

扩展类型加载器(ExClassLoader)

扩展类加载器是由Java语言进行编写的,父加载器是根类加载器,负责加载的是<JAVA_HOME>\jre\lib\ext

系统类加载器(Application ClassLoader )

系统类加载器也称之为应用类加载器,也是纯java类,他的父加载器是扩展类加载器,他负责从classpath环境变量
或者java.class.path所指定的目录中加载类,他是用户自定的类加载器的默认父加载器,该加载器是程序默认的类加载器,可以ClassLoader.getSystemClassLoader()直接获得

总结

jvm虚拟机class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将他的.class文件加载到内存上
生成class对象,而且加载某个类的.class文件时,jvm采用的是双亲委派机制,即将加载类的请求交由父加载器处理.

双亲委派机制

概念

除了根加载器之外,其他的类加载器都需要有自己的父加载器,双亲委派机制可以很好的保护java程序的安全,除了虚拟机自带的根加载器之外,其余的类加载器都有唯一的父加载机制.所以,在加载某个类的时候,会先让该类的类加载器委托自己的父加载器先去加载这个类,如果父加载器可以加载,则由父加载器进行加载,否则才使用该类的类加载器进行加载.即每个类加载器都很懒,加载类的时候都先让父加载器去尝试加载,一直到根加载器为止,加载不到的时候自己才去加载.
注意:双亲委派机制的父子关系并非是OOP当中的继承关系,而是通过使用组合模式来复用父加载器的代码.

双亲委派机制的优势

  1. 可以避免类被重复加载,当父加载器已经加载了该类的时候,就没有必要再使用子加载器进行再一次的加载了.
  2. 避免安全隐患.java核心API中定义的类型不能被随意更改,假设通过网络传递一个名为java.lang.Object的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心Java API发现了这个名字的类,发现该类已经被加载,所以就不会重新加载网络传递过来的java.lang.Object.class,这样可以方式核心的API库被随意篡改.

问题:我们可以自己定义一个Java.lang.String
在这里插入图片描述
执行结果:
在这里插入图片描述
原因:
程序在执行时识别的是src中的java.lang.Stringsrc就是classpath,这时我们自定义的类,所以会调用系统加载器。但根据双亲委派机制,系统加载器会逐层委派双亲来加载此类,在委派的时候,最上层的加载器是根加载器,即根加载器优先级最高。而根加载器能够在jre\lib\rt.jar包中找到一个重名的java.lang.String(即jdk自带的String),因此根据双亲委派最终会由最顶层的根加载器来执行jdk自带的java.lang.String。显然,jdk中的String并没有main()方法,因此报错找不到main()
也就是:我们最终加载到的还是java核心库中的String

ClassLoader

ClassLoader的源码中,有一个方法叫做Class<?> loadClass(String name, boolean resolve),这就是双亲委派模式的代码实现.
JVM尝试加载一个类时:会首先调用 loadClass 方法。loadClass 方法首先会检查该类是否已经被加载(即检查是否已经存在于JVM的类缓存中),如果没有,它会委托给父类加载器(如果有的话),直到到达引导类加载器(Bootstrap ClassLoader)。如果父类加载器无法加载该类,那么 loadClass 方法会调用 findClass 方法来尝试加载类。因此,findClass 方法的具体实现应该负责找到类的字节码,并将其传递给 defineClass 方法。

//name:包名+类名
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{      synchronized (getClassLoadingLock(name)) {// 1.先查询当前的所需的类是否已经被加载到了内存中Class<?> c = findLoadedClass(name);//2.没有被加载到内存中if (c == null) {long t0 = System.nanoTime();try {/* 双亲委派模式的实现*///如果当前不是父加载器,那么去父加载器中继续寻找if (parent != null) {c = parent.loadClass(name, false);} else {//是根加载器,那么就要从根加载器中获取该类c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {long t1 = System.nanoTime();//一直到根加载器,也没有找到该类//一般来说就是在自定义类被加载的时候//所以我们需要复写这个方法findClass(String name)c = findClass(name);// this is the defining class loader; record the statsPerfCounter.getParentDelegationTime().addTime(t1 - t0);PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;//这里返回的是一个class对象}
}
//由于我们要复写这个方法,所以这里给了一个默认的抛异常
protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}

步骤:

  • findLoadedClass(String) 通过该方法查找class是否已经载入内存,如果有,返回该class文件,没有到第二步
  • 通过loadClass方法,让父类先去载入,父类继续调用父类,循环至顶层的bootstarp加载器去加载.class文件
  • 看最后一段注释,findClass(String)方法一般定义在子类中,extClassLoader,AppClassLoaderfindClass方法定义在它们的父类(不是父加器)UrlClassLoader里面

findClass

public class BuiltinClassLoader extends SecureClassLoader
{
protected Class<?> findClass(String cn) throws ClassNotFoundException {if (!VM.isModuleSystemInited())throw new ClassNotFoundException(cn);LoadedModule loadedModule = findLoadedModule(cn);Class<?> c = null;if (loadedModule != null) {if (loadedModule.loader() == this) {c = findClassInModuleOrNull(loadedModule, cn);}} else {if (hasClassPath()) {c = findClassOnClassPathOrNull(cn);}}if (c == null)throw new ClassNotFoundException(cn);return c;
}
}

defineClass

用来byte字节解析成虚拟机能够识别的Class对象 ,defineClass()方法通常与findClass()方法一起使用,在自定义类加载器的时候,会直接覆盖ClassLoaderfindClass方法获取到要加载类的字节码文件,然后使用defineClass方法生成Class对象,即将字节码数组转换成Class对象

protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)throws ClassFormatError
{protectionDomain = preDefineClass(name, protectionDomain);String source = defineClassSourceLocation(protectionDomain);Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);postDefineClass(c, protectionDomain);return c;
}

loadClass,findClass,defineClass之间的关系

在这里插入图片描述

resolveClass

连接指定的类,类加载器就可以使用此方法来连接类。

URLClassLoader

java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用

  1. 构造方法:
  • public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器
//构造方法1
public class URLClassLoader extends SecureClassLoader implements Closeable {
//.......
public URLClassLoader(URL[] urls, ClassLoader parent) {super(parent);this.acc = AccessController.getContext();this.ucp = new URLClassPath(urls, acc);
}//.......
}
  • public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器
//构造方法2:
public class URLClassLoader extends SecureClassLoader implements Closeable {
//........
public URLClassLoader(URL[] urls) {super();this.acc = AccessController.getContext();this.ucp = new URLClassPath(urls, acc);
}//.......
}

案例一:加载磁盘上的类

解释一下URIURL

URI统一资源标识符)是一个用于标识某一互联网资源名称的字符串。它是对资源的位置、访问方式以及网络中资源的标识进行抽象的描述。URI有几种不同的类型,其中最常见的类型是URL统一资源定位符)和URN统一资源名称)。
URLURI一个子集,它不仅标识了资源,而且还提供了访问该资源的具体位置访问方法URL通常用于互联网上的网页地址,格式通常包括协议(如httphttps)、主机名、端口(可选)、路径以及查询参数等。例如,http://www.example.com:80/path/to/resource?query=parameter是一个URL

首先,现在D盘中建立一个名字为demo.java文件
在这里插入图片描述

public class test {public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {File file =new File("d:/");//将D盘中的资源转换成file的属性//URI和URLURI uri=file.toURI();//获取到file的uriURL url = uri.toURL();//获取到file的url//使用url对classLoader进行创建URLClassLoader classLoader=new URLClassLoader(new URL[]{url});System.out.println("父类加载器:"+classLoader.getParent());//找到父加载器//使用该类加载器,读取我们想要读取的全限定类名Class clazz=classLoader.loadClass("com.fbl.ClassLoader.demo");//返回一个字节码文件//进行实例化clazz.newInstance();}
}

运行结果:
在这里插入图片描述
打印出了构造方法中的语句demo instance,说明demo实例化成功

自定义类加载器

需要继承ClassLoader类,并且重写findClass方法
举例:

                    // 自定义一个类加载器  //
public class MyClassLoader extends ClassLoader{public String dic;//要加载的类所在的目录//1.建立两个构造方法public MyClassLoader(ClassLoader parent, String dic) {super(parent);this.dic = dic;}public MyClassLoader(String name) {this.dic = name;}//2.重写findClass方法:找到该类对应的字节码文件,转化成可以被JVM读取的二进制文件//这里的name:com.fbl.ClassLoader.demoprotected Class<?> findClass(String name) throws ClassNotFoundException{//1.获取到该类对应的字节码文件在磁盘上的绝对路径//com.fbl.classloader.demo  ->  D:/com/fbl/ClassLoader/demo.classString file=dic+File.separator+ name.replace(".",File.separator)+".class";System.out.println(dic);//2.构建输入流:int len= 0;byte buf[]= new byte[0];//字节码数组try {InputStream in=new FileInputStream(file);//3.构建字节输出流ByteArrayOutputStream out=new ByteArrayOutputStream();len = -1;buf = new byte[1024];while((len=in.read(buf))!=-1) {out.write(buf, 0, len);}in.close();out.close();} catch (IOException e) {throw new RuntimeException(e);}return defineClass(name,buf,0,len);}public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {MyClassLoader classLoader=new MyClassLoader("D:/");Class clazz=classLoader.loadClass("com.fbl.ClassLoader.demo");//在loadClass中会调用findClassclazz.newInstance();}
}

类的显式和隐式加载

显式加载

显式加载是指在java代码中通过调用ClassLoader加载class对象,比如Class.forName(String name)this.getClass().getClassLoader()来加载类。

隐式加载

隐式加载是指不需要在java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class时,该class引用了另外一个类的对象,那么这个对象的字节码文件会被虚拟机自动加载到内存中。


http://www.ppmy.cn/devtools/126495.html

相关文章

js 精确计算(加减乘除)

js 精确计算(加减乘除) 工具类CalcUtil.js class CalcUtil {// 两数相加calcAdd(num1, num2) {var r1, r2, m;try {r1 num1.toString().split(.)[1].length;} catch (e) {r1 0;}try {r2 num2.toString().split(.)[1].length;} catch (e) {r2 0;}m Math.pow(10, Math.max…

ESP32-C3实现非易失变量(Arduino IDE )

1效果 网页输入数据&#xff0c;串口打印数据。掉电后数据还在 2源码 #include <WiFi.h> // 包含WiFi库&#xff0c;用于处理WiFi连接 #include <WebServer.h> // 包含WebServer库&#xff0c;用于创建Web服务器 #include <Preferences.h> // 包含Prefere…

uni-app写的微信小程序如何体积太大如何处理

方法一&#xff1a;对主包进行分包处理&#xff0c;将使用url: /pages/components/equipment/equipment跳转页面的全部拆分为分包&#xff0c;如url: /pagesS/components/equipment/equipment 在pages.json中添加 "subPackages": [{ "root"…

TensorFlow 的核心概念

TensorFlow 是一个开源的机器学习框架&#xff0c;由 Google 开发和维护。它提供了一个强大的工具集&#xff0c;用于构建和训练各种机器学习模型。 TensorFlow 的核心概念是计算图&#xff08;Computational Graph&#xff09;。计算图由节点&#xff08;Nodes&#xff09;和…

开发与部署项目依赖管理之旅:Docker和venv区别

Docker与venv的区别 Docker和虚拟环境&#xff08;venv&#xff09;都是用于管理依赖关系和隔离环境的工具&#xff0c;但它们服务的目的和特点有所不同。下面是它们的详细比较。 虚拟环境&#xff08;venv&#xff09; 目的&#xff1a; 特定于Python&#xff1a;venv专为…

客户端、PC端、移动端集成发票真伪验真API接口返回值说明

客户端、pc端、移动端集成翔云发票查验接口&#xff0c;在现如今财务管理喝税务合规越来越严的背景下&#xff0c;集成发票真伪验真接口有助于提升财务工作效率&#xff0c;防止税务风险&#xff0c;简化发票管理流程&#xff0c;提升发票管理与报销效率&#xff0c;合规管理税…

JavaScript高级——ES6基础入门

前言 ES6&#xff08;ECMAScript 6&#xff09;&#xff0c;也被称为ES2015&#xff0c;是JavaScript的第六个版本.它于2015年发布&#xff0c;并在现代JavaScript开发中扮演了重要的角色. 学习方法: 看千遍&#xff0c;不如自己动手写一遍 遇到问题不要怕&#xff0c;微笑着…

5分钟上线你的简历

可以直接看视频&#xff1a;bilibilihttps://www.bilibili.com/video/BV1Af2fYhE7T/?share_sourcecopy_web&vd_sourceb8022571ef78f8f53b223aa5d1f31096 1.十块钱注册一个域名 2. 登录github去fork代码&#xff0c;地址&#xff1a;简历前端模版 3.用github登录vercel …