开发过程中经常会遇到java.lang.ClassNotFoundExcetpion的异常,为何会有这个异常呢?因为你想要使用的类并未加载到虚拟机当中。本文将介绍java的类加载器,让你了解一个class文件加载到虚拟机中的机制,并介绍了如何自定义类加载器。结合代码验证理论。
JVM预定义的三种类加载器
JVM预定义了三种类加载器,分别是:
- 启动类加载器(BootStrap)
- 扩展类加载器(Extension)
- 系统类加载器(System)
启动类加载器
启动类加载器是使用本地代码实现的,类似于我们看源码时的native方法,找不到具体的class。它负载将JAVA_HOME/lib下的核心类库或-Xbootclasspath选项指定的路径下的jar加载到虚拟机当中。
System.getProperty(“sun.boot.class.path”)参数可以查看该类加载器加载的路径。
因为没有具体的class,所以没法看类图。
扩展类加载器
扩展类加载器是sun.misc.Launcher$ExtClassLoader。它负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。
System.getProperty(“java.ext.dirs”)参数可以查看该类加载器加载的路径。
类图如下:
它是sun.misc.Launcher的内部类。
系统类加载器
系统类加载器是sun.misc.Launcher$AppClassLoader。它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径下的类库加载到内存中。开发者可以直接使用系统类加载器。
System.getProperty(“java.class.path”)可查看该类加载器加载的路径。
类图如下:
与扩展类加载器一样,它也是sun.misc.Launcher的内部类。
三种类加载的文件路径测试代码
下面的代码分别打印了三种类加载器加载了哪些路径的jar包,验证上面的介绍
package ClassLoader;
/**
* @author ludengke
* @title: ClassLoader.TestClassLoader
* @projectName springcloud-demo
* @description: TODO
* @date 2021/12/2615:15
*/
public class TestClassLoader {
public static void main(String[] args) {
bootStrapClassLoader();
extClassLoader();
appClassLoader();
// testClassLoader();
}
/**
* 测试TestBean被哪个类加载器加载的,这个加载器加载的路径是啥
*/
// private static void testClassLoader(){
// try {
// //查看当前系统类路径中包含的路径条目
// System.out.println(System.getProperty("java.class.path"));
// //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean
// Class typeLoaded = Class.forName("sun.net.spi.nameservice.dns.DNSNameService");
// Class typeLoaded2 = Class.forName("ClassLoader.TestBean");
// //查看被加载的TestBean类型是被那个类加载器加载的
// System.out.println(typeLoaded.getClassLoader());
// System.out.println(typeLoaded2.getClassLoader());
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
/***
*@Description 测试bootstrap加载的路径
*@Param []
*@Return void
*@Author ludengke
*@Date 2021/12/26
*@Time 15:17
*/
private static void bootStrapClassLoader(){
System.out.println("bootstrap start:");
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println("bootstrap end:");
}
/***
*@Description extClassLoader加载的路径
*@Param []
*@Return void
*@Author ludengke
*@Date 2021/12/26
*@Time 15:26
*/
private static void extClassLoader(){
//testClassLoaderParent();
System.out.println("ext start");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("ext end");
}
/**
* 获取类加载器的parent
*/
private static void testClassLoaderParent(){
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
/**
*@Description appClassLoader加载的路径
*@Param []
*@Return void
*@Author ludengke
*@Date 2021/12/26
*@Time 15:29
*/
private static void appClassLoader(){
System.out.println("app start");
System.out.println(System.getProperty("java.class.path"));
System.out.println("app end");
}
}
测试类执行结果如下:
D:\work\java\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=54689:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath D:\work\java\jre\lib\charsets.jar;D:\work\java\jre\lib\deploy.jar;D:\work\java\jre\lib\ext\access-bridge-64.jar;D:\work\java\jre\lib\ext\cldrdata.jar;D:\work\java\jre\lib\ext\dnsns.jar;D:\work\java\jre\lib\ext\jaccess.jar;D:\work\java\jre\lib\ext\jfxrt.jar;D:\work\java\jre\lib\ext\localedata.jar;D:\work\java\jre\lib\ext\nashorn.jar;D:\work\java\jre\lib\ext\sunec.jar;D:\work\java\jre\lib\ext\sunjce_provider.jar;D:\work\java\jre\lib\ext\sunmscapi.jar;D:\work\java\jre\lib\ext\sunpkcs11.jar;D:\work\java\jre\lib\ext\zipfs.jar;D:\work\java\jre\lib\javaws.jar;D:\work\java\jre\lib\jce.jar;D:\work\java\jre\lib\jfr.jar;D:\work\java\jre\lib\jfxswt.jar;D:\work\java\jre\lib\jsse.jar;D:\work\java\jre\lib\management-agent.jar;D:\work\java\jre\lib\plugin.jar;D:\work\java\jre\lib\resources.jar;D:\work\java\jre\lib\rt.jar;D:\work\springcloud-demo\study\target\classes;D:\work\maven\mavenstore\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar ClassLoader.TestClassLoader
bootstrap start:
D:\work\java\jre\lib\resources.jar;D:\work\java\jre\lib\rt.jar;D:\work\java\jre\lib\sunrsasign.jar;D:\work\java\jre\lib\jsse.jar;D:\work\java\jre\lib\jce.jar;D:\work\java\jre\lib\charsets.jar;D:\work\java\jre\lib\jfr.jar;D:\work\java\jre\classes
bootstrap end:
ext start
D:\work\java\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
ext end
app start
D:\work\java\jre\lib\charsets.jar;D:\work\java\jre\lib\deploy.jar;D:\work\java\jre\lib\ext\access-bridge-64.jar;D:\work\java\jre\lib\ext\cldrdata.jar;D:\work\java\jre\lib\ext\dnsns.jar;D:\work\java\jre\lib\ext\jaccess.jar;D:\work\java\jre\lib\ext\jfxrt.jar;D:\work\java\jre\lib\ext\localedata.jar;D:\work\java\jre\lib\ext\nashorn.jar;D:\work\java\jre\lib\ext\sunec.jar;D:\work\java\jre\lib\ext\sunjce_provider.jar;D:\work\java\jre\lib\ext\sunmscapi.jar;D:\work\java\jre\lib\ext\sunpkcs11.jar;D:\work\java\jre\lib\ext\zipfs.jar;D:\work\java\jre\lib\javaws.jar;D:\work\java\jre\lib\jce.jar;D:\work\java\jre\lib\jfr.jar;D:\work\java\jre\lib\jfxswt.jar;D:\work\java\jre\lib\jsse.jar;D:\work\java\jre\lib\management-agent.jar;D:\work\java\jre\lib\plugin.jar;D:\work\java\jre\lib\resources.jar;D:\work\java\jre\lib\rt.jar;D:\work\springcloud-demo\study\target\classes;D:\work\maven\mavenstore\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar
app end
Process finished with exit code 0
从测试结果可以看出系统类加载器也加载了启动类加载器、扩展类加载应该加载的路径,这会不会有问题呢?
应该不会,下面介绍类加载机制。
类加载机制(双亲委派机制)
类加载器的继承关系如下,这里的继承关系不是真正的继承,是类加载器的父子委托关系
双亲委派机制:
- 当类加载器接收到加载某个类的请求时,先看看该类加载器是否加载过此类,若加载过则不需要加载,直接使用,若未加载过此类,则委托给父类加载器加载。
- 以此类推,直至BootStrapClassLoader。
- 若BootStrapClassLoader加载不到该类,则委托给它的子类加载器ExtClassLoader加载。
- 以此类推,直至最底层的类加载器。
- 若都加载不到,那就是找不到该类的错误了。
双亲委派机制的作用:
为什么这么设计呢?
从这个加载机制可以看出顶层的类加载器优先级高很多,会覆盖掉底层类加载器加载的类。
委托给父类加载器时,若父类已经加载该类,则直接用,不需要再重新加载,且保证不覆盖父类加载器加载的内容,保证数据安全。
上层的类加载器实在加载不到,不能憋着,只能儿孙们自己动手加载。不然底层加载器没有了。
总结起来一句话:孩子们很懒,已经加载过该类则直接用,否则找长辈们要。如果长辈们也没有,孩子们才自己动手加载。 - 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
测试类加载机制
- 测试代码如下
随便写一个类TestBean
package ClassLoader;
public class TestBean {
public TestBean() {
}
}
测试类,主要看testClassLoader()方法
package ClassLoader;
/**
* @author ludengke
* @title: ClassLoader.TestClassLoader
* @projectName springcloud-demo
* @description: TODO
* @date 2021/12/2615:15
*/
public class TestClassLoader {
public static void main(String[] args) {
// bootStrapClassLoader();
// extClassLoader();
// appClassLoader();
testClassLoader();
}
/**
* 测试TestBean被哪个类加载器加载的,这个加载器加载的路径是啥
*/
private static void testClassLoader(){
try {
//查看当前系统类路径中包含的路径条目
// System.out.println(System.getProperty("java.class.path"));
//调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean
Class typeLoaded = Class.forName("sun.net.spi.nameservice.dns.DNSNameService");
Class typeLoaded2 = Class.forName("ClassLoader.TestBean");
//查看被加载的TestBean类型是被那个类加载器加载的
System.out.println(typeLoaded.getClassLoader());
System.out.println(typeLoaded2.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
/***
*@Description 测试bootstrap加载的路径
*@Param []
*@Return void
*@Author ludengke
*@Date 2021/12/26
*@Time 15:17
*/
private static void bootStrapClassLoader(){
System.out.println("bootstrap start:");
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println("bootstrap end:");
}
/***
*@Description extClassLoader加载的路径
*@Param []
*@Return void
*@Author ludengke
*@Date 2021/12/26
*@Time 15:26
*/
private static void extClassLoader(){
// testClassLoaderParent();
System.out.println("ext start");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("ext end");
}
/**
* 获取类加载器的parent
*/
private static void testClassLoaderParent(){
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
/**
*@Description appClassLoader加载的路径
*@Param []
*@Return void
*@Author ludengke
*@Date 2021/12/26
*@Time 15:29
*/
private static void appClassLoader(){
System.out.println("app start");
System.out.println(System.getProperty("java.class.path"));
System.out.println("app end");
}
}
- 运行测试类
sun.misc.Launcher$ExtClassLoader@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2
Process finished with exit code 0
- sun.net.spi.nameservice.dns.DNSNameService这个类是扩展类加载器加载的路径中的类,暂时不看。主要看ClassLoader.TestBean,该类是用AppClassLoader加载的,加载的是上面写的那个类。
- 将ClassLoader.TestBean类生成testjar.jar,将该jar包拷贝到JAVA_HOME\jre\lib\ext下,再次运行,结果如下。可以看出ClassLoader.TestBean类是ExtClassLoader类加载器加载的,加载的是JAVA_HOME\jre\lib\ext下testjar的内容。可以验证双亲委派机制。
D:\work\java\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=62498:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath D:\work\java\jre\lib\charsets.jar;D:\work\java\jre\lib\deploy.jar;D:\work\java\jre\lib\ext\access-bridge-64.jar;D:\work\java\jre\lib\ext\cldrdata.jar;D:\work\java\jre\lib\ext\dnsns.jar;D:\work\java\jre\lib\ext\jaccess.jar;D:\work\java\jre\lib\ext\jfxrt.jar;D:\work\java\jre\lib\ext\localedata.jar;D:\work\java\jre\lib\ext\nashorn.jar;D:\work\java\jre\lib\ext\sunec.jar;D:\work\java\jre\lib\ext\sunjce_provider.jar;D:\work\java\jre\lib\ext\sunmscapi.jar;D:\work\java\jre\lib\ext\sunpkcs11.jar;D:\work\java\jre\lib\ext\zipfs.jar;D:\work\java\jre\lib\javaws.jar;D:\work\java\jre\lib\jce.jar;D:\work\java\jre\lib\jfr.jar;D:\work\java\jre\lib\jfxswt.jar;D:\work\java\jre\lib\jsse.jar;D:\work\java\jre\lib\management-agent.jar;D:\work\java\jre\lib\plugin.jar;D:\work\java\jre\lib\resources.jar;D:\work\java\jre\lib\rt.jar;D:\work\springcloud-demo\study\target\classes;D:\work\maven\mavenstore\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar ClassLoader.TestClassLoader
sun.misc.Launcher$ExtClassLoader@7f31245a
sun.misc.Launcher$ExtClassLoader@7f31245a
Process finished with exit code 0
- 将testjar.jar拷贝到JAVA_HOME/lib 下,再次运行测试类,结果如下。按理来说启动类加载器应该加载testjar.jar中的ClassLoader.TestBean,可是没有。为啥呢?你什么都往里面写,jdk就不安全了,所以他不认识的东西不加载,保证了安全。
D:\work\java\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=62680:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath D:\work\java\jre\lib\charsets.jar;D:\work\java\jre\lib\deploy.jar;D:\work\java\jre\lib\ext\access-bridge-64.jar;D:\work\java\jre\lib\ext\cldrdata.jar;D:\work\java\jre\lib\ext\dnsns.jar;D:\work\java\jre\lib\ext\jaccess.jar;D:\work\java\jre\lib\ext\jfxrt.jar;D:\work\java\jre\lib\ext\localedata.jar;D:\work\java\jre\lib\ext\nashorn.jar;D:\work\java\jre\lib\ext\sunec.jar;D:\work\java\jre\lib\ext\sunjce_provider.jar;D:\work\java\jre\lib\ext\sunmscapi.jar;D:\work\java\jre\lib\ext\sunpkcs11.jar;D:\work\java\jre\lib\ext\zipfs.jar;D:\work\java\jre\lib\javaws.jar;D:\work\java\jre\lib\jce.jar;D:\work\java\jre\lib\jfr.jar;D:\work\java\jre\lib\jfxswt.jar;D:\work\java\jre\lib\jsse.jar;D:\work\java\jre\lib\management-agent.jar;D:\work\java\jre\lib\plugin.jar;D:\work\java\jre\lib\resources.jar;D:\work\java\jre\lib\rt.jar;D:\work\springcloud-demo\study\target\classes;D:\work\maven\mavenstore\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar ClassLoader.TestClassLoader
sun.misc.Launcher$ExtClassLoader@7f31245a
sun.misc.Launcher$ExtClassLoader@7f31245a
Process finished with exit code 0
自定义类加载器
为什么需要自定义类加载器呢?
因为系统的ClassLoader只会加载指定目录下的class文件,如果你想加载自己的class文件,那么就可以自定义一个ClassLoader。
而且可以使用加密的class文件,加载该class文件时,只有懂得如何解密才能使用,这时也需要自定义的类加载器。
而且我们可以根据自己的需求,对class文件进行加密和解密。
- 编写被加载的类,里面随便写个方法,代码如下:
package ClassLoader;
public class TestBean {
public TestBean() {
}
public void testMethod(){
System.out.println("ClassLoader.TestBean.testMethod()");
}
}
- 编写自定义的类加载器MyClassLoader,继承java.lang.ClassLoader,重写findClass方法,代码如下:
package ClassLoader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @author ludengke
* @title: MyCloassLoader
* @projectName springcloud-demo
* @description: TODO
* @date 2022/1/522:41
*/
public class MyCloassLoader extends ClassLoader {
private String path;
public MyCloassLoader(String path) {
this.path = path;
}
/**
* 重父类的该方法
*
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class log = null;
// 获取该class文件字节码数组
byte[] classData = getData();
if (classData != null) {
// 将class的字节码数组转换成Class类的实例
log = defineClass(name, classData, 0, classData.length);
}
return log;
}
/**
* 获取class文件的字节码
* @return
*/
private byte[] getData() {
File file = new File(path);
if (file.exists()) {
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
} else {
return null;
}
}
}
- 编写测试类,代码如下:
package ClassLoader;
import java.lang.reflect.Method;
/**
* @author ludengke
* @title: ClassLoader.TestClassLoader
* @projectName springcloud-demo
* @description: TODO
* @date 2021/12/2615:15
*/
public class TestClassLoader {
public static void main(String[] args) {
testMyClassLoader();
}
/**
* 测试自定义的类加载器
*/
private static void testMyClassLoader(){
try {
//实例类加载器
MyCloassLoader cloassLoader=new MyCloassLoader("D:\\work\\springcloud-demo\\study\\target\\classes\\ClassLoader\\TestBean.class");
System.out.println("自定义的类加载器的父类加载器是:"+cloassLoader.getParent());
//通过自定义的类加载器加载类
Class testBeanClass=cloassLoader.findClass("ClassLoader.TestBean");
//加载出来的类的类加载器
System.out.println("类加载器是:" + testBeanClass.getClassLoader());
//通过反射调用类的方法
Method method = testBeanClass.getDeclaredMethod("testMethod");
Object object = testBeanClass.newInstance();
//调用类的方法
method.invoke(object);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 运行测试类,结果如下:
D:\work\java\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=63921:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath D:\work\java\jre\lib\charsets.jar;D:\work\java\jre\lib\deploy.jar;D:\work\java\jre\lib\ext\access-bridge-64.jar;D:\work\java\jre\lib\ext\cldrdata.jar;D:\work\java\jre\lib\ext\dnsns.jar;D:\work\java\jre\lib\ext\jaccess.jar;D:\work\java\jre\lib\ext\jfxrt.jar;D:\work\java\jre\lib\ext\localedata.jar;D:\work\java\jre\lib\ext\nashorn.jar;D:\work\java\jre\lib\ext\sunec.jar;D:\work\java\jre\lib\ext\sunjce_provider.jar;D:\work\java\jre\lib\ext\sunmscapi.jar;D:\work\java\jre\lib\ext\sunpkcs11.jar;D:\work\java\jre\lib\ext\zipfs.jar;D:\work\java\jre\lib\javaws.jar;D:\work\java\jre\lib\jce.jar;D:\work\java\jre\lib\jfr.jar;D:\work\java\jre\lib\jfxswt.jar;D:\work\java\jre\lib\jsse.jar;D:\work\java\jre\lib\management-agent.jar;D:\work\java\jre\lib\plugin.jar;D:\work\java\jre\lib\resources.jar;D:\work\java\jre\lib\rt.jar;D:\work\springcloud-demo\study\target\classes;D:\work\maven\mavenstore\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar ClassLoader.TestClassLoader
自定义的类加载器的父类加载器是:sun.misc.Launcher$AppClassLoader@18b4aac2
类加载器是:ClassLoader.MyCloassLoader@1540e19d
ClassLoader.TestBean.testMethod()
Process finished with exit code 0
- 分析:
1、自定义的类加载器需要继承java.lang.ClassLoader,重写findClass方法。
2、findClass方法的内容是加载指定路径下的class文件,将其转为字节数组,调用父类的defineClass方法将字节数组转为Class对象。
3、通过测试类可以看出,自定义类加载器的父类加载器是sun.misc.Launcher$AppClassLoader。
4、通过测试结果可以看出,自定义类加载器成功加载指定路径下的class文件,设计成功。
参考博客
https://www.jianshu.com/p/1e4011617650
https://blog.csdn.net/huazai30000/article/details/85296671