文章目录
- 类加载器的概述
- 类加载器的分类
- 启动类加载器(`Bootstrap ClassLoader `)
- 扩展类型加载器(ExClassLoader)
- 系统类加载器(Application ClassLoader )
- 总结
- 双亲委派机制
- 概念
- 双亲委派机制的优势
- ClassLoader
- `findClass`
- `defineClass`
- loadClass,findClass,defineClass之间的关系
- resolveClass
- URLClassLoader
- 案例一:加载磁盘上的类
- 解释一下`URI`和`URL`
- 自定义类加载器
- 类的显式和隐式加载
- 显式加载
- 隐式加载
类加载器的概述
Java
中的类加载机制是指在Java
程序运行时,将类文件(通常是.class
文件)加载到内存中的一系列步骤和过程。这一机制确保了类能够在需要的时候被正确、安全地加载到Java
虚拟机(JVM
)中,并进行初始化和使用。Java
的类加载机制遵循着“按需加载”原则,即只有在需要用到某个类的时候,才会加载该类。
简单来说:ClassLoader
是Java
的核心组件,所有的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
当中的继承关系,而是通过使用组合模式来复用父加载器的代码.
双亲委派机制的优势
- 可以避免类被重复加载,当父加载器已经加载了该类的时候,就没有必要再使用子加载器进行再一次的加载了.
- 避免安全隐患.
java
核心API
中定义的类型不能被随意更改,假设通过网络传递一个名为java.lang.Object
的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心Java API
发现了这个名字的类,发现该类已经被加载,所以就不会重新加载网络传递过来的java.lang.Object.class
,这样可以方式核心的API
库被随意篡改.
问题:我们可以自己定义一个
Java.lang.String
吗
执行结果:
原因:
程序在执行时识别的是src
中的java.lang.String
,src
就是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
,AppClassLoader
的findClass
方法定义在它们的父类(不是父加器)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()
方法一起使用,在自定义类加载器的时候,会直接覆盖ClassLoader
的findClass
方法获取到要加载类的字节码文件,然后使用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
,能够从本地或者网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用。
- 构造方法:
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);
}//.......
}
案例一:加载磁盘上的类
解释一下URI
和URL
URI
(统一资源标识符)是一个用于标识某一互联网资源名称的字符串。它是对资源的位置、访问方式以及网络中资源的标识进行抽象的描述。URI
有几种不同的类型,其中最常见的类型是URL
(统一资源定位符)和URN
(统一资源名称)。
URL
是URI
的一个子集,它不仅标识了资源,而且还提供了访问该资源的具体位置和访问方法。URL
通常用于互联网上的网页地址,格式通常包括协议(如http
、https
)、主机名、端口(可选)、路径以及查询参数等。例如,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
引用了另外一个类的对象,那么这个对象的字节码文件会被虚拟机自动加载到内存中。