【JVM】一文详解类加载器

server/2024/10/17 14:38:38/

文章目录

  • 类加载器的概述
  • 类加载器的分类
    • 启动类加载器(`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/server/132503.html

相关文章

使用DeepKE训练命名实体识别模型DEMO(官方DEMO)

使用DeepKE训练命名实体识别模型DEMO&#xff08;官方DEMO&#xff09; 说明&#xff1a; 首次发表日期&#xff1a;2024-10-10DeepKE资源&#xff1a; 文档&#xff1a; https://www.zjukg.org/DeepKE/网站&#xff1a; http://deepke.zjukg.cn/cnschema&#xff1a; http:/…

Java设计模式梳理:行为型模式(策略,观察者等)

行为型模式 行为型模式关注的是各个类之间的相互作用&#xff0c;将职责划分清楚&#xff0c;使得我们的代码更加地清晰。 策略模式 策略模式太常用了&#xff0c;所以把它放到最前面进行介绍。它比较简单&#xff0c;我就不废话&#xff0c;直接用代码说事吧。 下面设计的…

用示波器观测RC一阶电路零输入响应是否激励必须是方波信号

概述 RC一阶电路是一种简单但非常重要的电路&#xff0c;广泛应用于滤波、信号处理和时间常数分析等领域。在研究RC电路的动态特性时&#xff0c;零输入响应&#xff08;Natural Response&#xff09;是一项关键内容。本文将详细解析用示波器观测RC一阶电路零输入响应时&#…

Spring Boot助力B2B医疗平台病历数据交换

第1章绪论 计算机已经从科研院所&#xff0c;大中型企业&#xff0c;走进了平常百姓家&#xff0c;Internet遍及世界各地&#xff0c;在网上能够用计算机进行文字草拟、修改、打印清样、文件登陆、检索、综合统计、分类、数据库管理等&#xff0c;用科学的方法将无序的信息进行…

不用搭建服务?MemFire Cloud让开发更简单

不用搭建服务&#xff1f;MemFire Cloud让开发更简单 在当今的开发世界里&#xff0c;想要开发一个功能齐全的应用&#xff0c;往往意味着需要搭建复杂的后端、开发API接口、处理认证授权、管理数据库……这些琐碎的工作让很多开发者头疼不已&#xff0c;尤其是独立开发者或者…

菜鸟笔记006 截图识别文字插件 textOCR

随手可得的截图识别文字插件 textOCR&#xff0c;识别出来的文字可直接输入到illustrator的当前文档中&#xff1a; 执行条件 1、需截图软件支持&#xff0c;推荐笔记截图工具 2、截好图片直接拖入面板即可完成识别 ****后期可完成实现在illustrator选择图片对象完成文字识别。…

经典困难难度算法题,利用优先队列其实很好解决

这道题在力扣官网上面是困难难度。但是假如利用好 C 的优先队列或者利用好大根堆和小根堆&#xff0c;​这道题的思路其实很简单。 题目描述&#xff1a; 题号&#xff1a;295 中位数是有序整数列表中的中间值。如果列表的大小是偶数&#xff0c;则没有中间值&#xff0c;中位…

自定义注解和组件扫描在Spring Boot中动态注册Bean(一)

​ 博客主页: 南来_北往 系列专栏&#xff1a;Spring Boot实战 在Spring Boot中&#xff0c;自定义注解和组件扫描是两种强大的机制&#xff0c;它们允许开发者以声明性的方式动态注册Bean。这种方式不仅提高了代码的可读性和可维护性&#xff0c;还使得Spring Boot应用的…