虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现。实现这个动作的代码模块称为类加载器。
类加载器虽然用于实现类的加载动作,但它在java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。
一、类加载器的作用
java程序在运行的时候,JVM通过类加载机制(ClassLoader)把class文件加载到内存中,只有class文件被载入内存,才能被其他class引用,使程序正确运行起来。
二、类加载器的分类
java中ClassLoader的分类有三种:启动类加载器、扩展类加载器、应用程序类加载器
- 启动类加载器(Bootstrap ClassLoader):由C++编写,由JVM启动,启动类加载器负责加载java基础类,对应的文件是%JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar和class等。启动类加载器无法被java程序直接引用,在编写自定义类加载器时,如果需要把请求委派给引导类加载器,直接使用null代替。
- 扩展类加载器(Extension ClassLoader):这个加载器负责加载JAVA_HOME\lib\ext目录下中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般这个就是程序中默认的类加载器。
三、双亲委派模型
双亲委派模型工作过程:如果一个类加载器收到类加载请求,它首先不会自己亲自去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每个加载器都是如此,因此所有的请求最终都应该传送到顶层的启动类加载器中,只有当父加载器在自己搜索范围内找不到指定的类时(ClassNotFoundException),子加载器才会尝试自己去加载。
四、认识ClassLoader类加载器代码
1.loadClass()
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空则调用父加载器的loadClass()方法
c = parent.loadClass(name, false);
} else {
//父加载器为空,调用BootstrapClassLoader(启动类加载器)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//父加载器没有找到,就调用findClass()方法
long t1 = System.nanoTime();
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();
}
}
//调用resolveClass()
if (resolve) {
resolveClass(c);
}
return c;
}
}
loadClass()函数即实现了双亲委派模型,大致过程如下:
- 首先检查指定全限定名的类是否已经加载过。如果加载了,就不需要再次加载,直接返回。
- 如果此类没有被加载,那么判断是否有类加载器;如果有,则由父加载器加载(调用parent.loadClass(name,false)方法)或者调用Bootstrap类加载器加载。
- 如果父加载器及Bootstrap类加载器都没有找到指定的类,那么就调用当前类加载器的findClass()方法来完成加载。
所以,如果自定义类加载器,必须重写findClass()方法。
2.findClass()
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
ClassLoader类中的findClass函数默认是直接抛出异常。在loadClass中提到,在父加载器无法加载类的时候,就会调用自定义的类加载器中的findClass()函数,因此必须要在loadClass这个函数里实现将一个指定类名称转换为Class对象。
java提供了defineClass()方法可以把一个字节数组转换为Class对象。
3.defineClass()
@Deprecated
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
defineClass()将一个字节数组转为Class对象,这个字节数组是class文件读取后的字节数组,如果class文件是加密过的,则需要解密后作为参数传入defineClass函数。
五、自定义类加载器
-
新建一个类继承java.lang.ClassLoader,重写findClass()方法。
-
将class字节码数组转换为Class类的实例。使用defineClass()方法。
-
调用loadClass()方法。
1.自定义类加载器
继承ClassLoader,重写findClass方法,使用defineClass将class文件字节数组转换Class类实例。
package com.simon.loader;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 新建自定义加载器 继承ClassLoader
*/
public class MyNewClassLoader extends ClassLoader {
private String classPath;
public MyNewClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 重写findClass方法
* @param name 全限定类名的类
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//将全限定名的class类文件读取为字节流数组
byte[] data = loadByte(name);
//使用defineClass方法将字节流数组作为Class对象返回
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 将全限定名的class类文件读取为字节流数组
*/
private byte[] loadByte(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
2.定义需要自定义类加载器加载的class文件
package com.simon.classloader;
public class Log {
public void hello() {
System.out.println(getClass().getClassLoader().getClass());
}
}
将Log.java拷贝到单独的文件夹中
编译Log.java
3.启动类使用自定义类加载class文件
调用自定义的loadClass方法
package com.simon.loader;
import com.simon.classloader.Main;
import java.lang.reflect.Method;
public class Application {
public static void main(String args[]) throws Exception {
MyNewClassLoader classLoader = new MyNewClassLoader("D:\\test");
Class clazz = classLoader.loadClass("com.simon.classloader.Log");
Object obj = clazz.newInstance();
Method helloMethod = clazz.getDeclaredMethod("hello", null);
helloMethod.invoke(obj, null);
}
}
4.执行结果