类加载器顾名思义就是加载类的工具,在Java中用到类的时候,JVM首先要把类的字节码文件加载到内存中来,大家可能在开发的过程中经常会遇到java.lang.ClassNotFoundExcetpion这个异常,出现这种异常的原因就是类加载器找不到要加载的类了
先看一下段代码
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
while (classLoader !=null){
System.out.println("classLoaderName:"+classLoader.getClass().getName());
classLoader = classLoader.getParent();
}
}
}
在这个小例子中,为我们打印出了两个类加载器,其实在Java中JVM预定义了三种类加载器,每个类加载器都有自己所负责的加载类的范围
三种预定义的类加载器分别是
- Bootstrap:引导类加载器,最顶层的一个类加载器,这个类加载器不是Java类它是C++写的二进制代码,它嵌套在JVM内核里,也就是说JVM启动的时候BootStrap就启动了,负责加载核心Java库,存储在<JAVA_HOME>/jre/lib/rt.jar 中的类,或者加载-Xbootclasspath选项指定的jar包
ClassLoader intClassLoader = int.class.getClassLoader();
System.out.println("intClassLoader="+intClassLoader);//intClassLoader=null
如上两行代码,随便从rt.jar包中拉出来一个类,我们打印它的类加载器,结果为空,因为他们的类加载器是Bootstrap,它不是JAVA类,所以返回null
- ExtClassLoader :扩展类加载器,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 加载到内存中
- AppClassLoader:系统类加载器, 它负责将 (java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库 加载到内存中
说到类加载器,不得不说一下类加载双亲委派机制,如下图
JVM在加载类时默认采用的是双亲委派机制,当AppClassLoader收到加载类的需求时,AppClassLoader委托给它的上一级ExcClassLoader,ExcClassLoader又委托给Bootstrap去加载,Bootstrap就去它负责的jar包或目录下去查找要加载的.class文件,当他找到的时候就返回,如果它找不到,它就告诉ExcClassLoader说我找不到加载不了,你去加载吧,ExcClassLoader就去它负责的jar包和目录下去查找,它找到就返回,找不到就告诉AppClassLoader说,我也找不到加载不了,你去加载吧,AppClassLoader就去找,这样一级级的往下查找,当AppClassLoader也找不到需要加载的类时,就会抛出ClassNotFoundExcetpion异常。需要说明的是当所需要加载的类指定了某个高级的类加载器加载时,它不会向比它更低级的类加载器去加载,就像AppClassLoader找不到需要加载的类时,不会向比它低级的MyClassLoad1和MyClassLoad2去查找
JVM在加载第一个类的时候到底使用哪个类加载器呢?
先它会使用当前线程的类加载器去加载线程中的第一个类,thread.getContextClassLoader();可以获取到线程的类加载器,可以thread.setContextClassLoader(classLoader);设置线程的类加载器。如果类A中引用了类B,JVM将使用类A的ClassLoader来加载类B。也可以指定类加载器去加载一个类ClassLoader.loadClass("com.lp.beans.PersonBean");
自定义类加载器
AppClassLoader的类继承关系图
ExtClassLoader的类继承关系图
我们看到都会继承ClassLoader这个抽象类,自定义类加载器也需要继承这个抽象类。自定义类加载器,需要用到三个ClassLoader中的三个方法,loadClass,findClass,defineClass。下面我们看看他们都做了什么
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//异步锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded 首先检查这个类是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {//没有被加载过,执行下面的逻辑
// -----------------------------------------------------------------
/**
* 中间这一块的逻辑就是,把需要加载的类,使用委托机制交给所有的上级类加载器去加载
* 如果加载到了,那就返回class,如果找不到,就使用findClass 方法自己查找
* 我们如果自己定义类加载器只需要覆盖findClass方法定义如何查找就可以了
*
* 如果我们重定义了loadClass,那么就不会从上级,上上级。。。loader 中加载类了,
* 除非我们自己定义从上级查找逻辑,
*/
long t0 = System.nanoTime();
try {
//如果它的上一级加载器存在,则使用它的上一次加载器 加载需要加载的类
if (parent != null) {
//这里递归调用
c = parent.loadClass(name, false);
} else {//如果上一级加载器不存在,则使用Bootstrap加载需要加载的类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 所有上级都找不到需要加载的类 抛出异常 这里什么都没做,是还需要执行下面的逻辑
}
// -----------------------------------------------------------------
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//如果它的上级,上上级。。。loader 都找不到需要加载的类,那么就使用自己的loader来加载类
long t1 = System.nanoTime();
//这里使用findClass来加载类,自己定义查找逻辑
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//解析类
resolveClass(c);
}
return c;
}
}
findClass,其实里面的逻辑很简单,自己要从哪个地方什么路径下去加载.class文件,之后怎么加载到JVM呢,就是通过defineClass方法,我们看到它是一个final方法,所有我们直接调用它就可以了,具体里面有加载前预处理,和加载逻辑预计加载后的东西大家去看吧
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(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
了解上面的内容后,我们开始定义自己的类加载器
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String fullName) throws ClassNotFoundException {
try {
//将类名转换成路径 com.lp.loader.Person 转换成 com\lp\loader\Person.class
fullName = fullName.replace(".", File.separator)+".class";
//根据路径读取.class 文件
FileInputStream in = new FileInputStream(classPath + fullName);
//转换成字节
byte[] b = new byte[in.available()];
in.read(b);
//调用父类提供的 defineClass 获取class
return defineClass(null, b,0,b.length);
} catch ( Exception e) {
e.printStackTrace();
}
return null;
}
}
然后我们再定义一个Person类,如下
public class Person {
@Override
public String toString() {
return "Person{}";
}
}
找到它再类路径下的Person.class ,复制一份到E:\\class,把这个Person类删除,原来类路径下的Person.class也删除
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
//获取classpath E:/test/out/production/test/ 这是我们当前项目的类路径
// String classPath = Thread.currentThread().getContextClassLoader().getResource("").getFile();
//定义类路径 我们把需要加载的文件放到这下面
String classPath = "E:\\class\\";
//创建自定义类加载器对象
MyClassLoader myClassLoader = new MyClassLoader(classPath);
//加载 E:\class\ 路径下的类
Class<?> clazz = myClassLoader.loadClass("Person");
//加载除了的类创建对象
Object obj = clazz.newInstance();
//获取toString方法
Method toString = clazz.getMethod("toString");
//执行方法 打印
System.out.println(toString.invoke(obj));
}
}
打印结果
这样我们的类加载器就完成了,可以加载任何路径下的.class文件