文章目录
- 类加载器的概述
- 类加载器的分类
-
- [启动类加载器(`Bootstrap ClassLoader `)](#启动类加载器(
Bootstrap ClassLoader
)) - 扩展类型加载器(ExClassLoader)
- [系统类加载器(Application ClassLoader )](#系统类加载器(Application ClassLoader ))
- 总结
- [启动类加载器(`Bootstrap ClassLoader `)](#启动类加载器(
- 双亲委派机制
- ClassLoader
- URLClassLoader
- 类的显式和隐式加载
类加载器的概述
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 stats
PerfCounter.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和URL
URI uri=file.toURI();//获取到file的uri
URL 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.demo
protected Class<?> findClass(String name) throws ClassNotFoundException{
//1.获取到该类对应的字节码文件在磁盘上的绝对路径
//com.fbl.classloader.demo -> D:/com/fbl/ClassLoader/demo.class
String 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中会调用findClass
clazz.newInstance();
}
}
类的显式和隐式加载
显式加载
显式加载 是指在java
代码中通过调用ClassLoader
加载class
对象,比如Class.forName(String name)
;this.getClass().getClassLoader()
来加载类。
隐式加载
隐式加载是指不需要在java
代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class
时,该class
引用了另外一个类的对象,那么这个对象的字节码文件会被虚拟机自动加载到内存中。