前言
顾名思义,类的加载器就是负责类的加载职责,每一个class都需要由它的类加载器和这个类本身确立其在JVM中的唯一性,这也就是运行时包。
JVM中内置的三大类型加载器
JVM提供了三种累加器,分别是BootStrap Class Loader
,Ext Class Loader
,Application ClassLoader
,不同的类加载器负责不同的类加载到JVM内存之中,并且它们之间严重遵循着父委托机制。
根类加载器介绍
根类加载器也称Bootstrap 加载器,它是由C++编写的,主要负责虚拟机核心类库的加载,比如整合java.lang包就是根类加载器加载的。
根类加载器是获取不到引用的,比如你想获取String.class.getClassLoader()时,程序会返回null
扩展类加载器
扩展类加载器的父加载器时根类加载器,它主要加载JAVA_HOME下的jre/lib/ext子目录下的类库。该加载器是纯JAVA语言编写的。可以通过系统属性java.ext.dirs
获得。
可以将自己打好的jar放到扩展类加载器的路径下面,然后获取jar包中类的加载器,就是扩展类加载器。
系统类加载器
系统类加载器的父加载器是扩展类加载器,加载的是项目中classpath下的jar,同时可以通过-cp命令指定加载的路径,我们自定义的加载器的父类也是系统类加载器,
可以通过系统命令java.class.path获取加载路径。
自定义类加载器
所有自定义类加载器都是ClassLoader的子类或者间接子类,java.lang.ClassLoader是一个抽象类,它里面没有抽象方法,但是有findClass方法,务必实现该方法,否则会抛出Class找不到的异常,示例代码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
}
自定义类加载器,问候世界
public class MyClassloader extends ClassLoader {
private final static Path DEFAULT_CLASS_DIR = Paths.get("C:", "classload1");
private final Path classDir;
public MyClassloader() {
this.classDir = DEFAULT_CLASS_DIR;
}
public MyClassloader(String classDir) {
super();
this.classDir = Paths.get(classDir);
}
public MyClassloader(String classDir, ClassLoader parent) {
super(parent);
this.classDir = Paths.get(classDir);
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
//设置class的二进制数据
byte[] classBytes = this.readClassBytes(name);
if (null == classBytes || classBytes.length == 0) {
throw new ClassNotFoundException("can not load this class" + name);
}
//调用defineClass方法定义class
return this.defineClass(name, classBytes, 0, classBytes.length);
}
private byte[] readClassBytes(String name) throws ClassNotFoundException {
String classPath = name.replace(".", "/");
Path classFullPath = classDir.resolve(Paths.get(classPath + ".class"));
if (!classFullPath.toFile().exists()) {
throw new ClassNotFoundException("not fount class:" + name);
}
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Files.copy(classFullPath, baos);
return baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("this class" + name + "error:" + e);
}
}
@Override
public String toString() {
return "MyClass Loader";
}
}
以上就是自定义的类加载器,通过类的全名称转换为文件的全路径重写findClass方法,然后读取class文件的字节流,最后用ClassLoader的defineClass方法对类进行了定义。
下面写一个简单的程序,说明下
public class MyClassLoaderTest {
public static void main(String[] args)
throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassloader myClassloader = new MyClassloader();
Class<?> clazz = myClassloader.loadClass("fast.cloud.nacos.juc.classloader.HelloWorld");
System.out.println(clazz.getClassLoader());
//1
Object helloWorld = clazz.newInstance();
System.out.println(helloWorld);
Method welcomeMethod = clazz.getMethod("welcome");
Object result = welcomeMethod.invoke(helloWorld);
System.out.println("Result: " + result);
}
}
具体操作步骤:
编译HelloWorld文件,注意带上 -d .
会同时把包路径也生成好的。
删除HelloWorld.java文件
运行MyClassLoaderTest文件
如果没有错误发生,程序会输出类加载器的以及对世界的问候,注释掉1下面的代码,输出类加载器的信息,但是Hello World的静态代码块并没有输出,类加载loadClass并不会导致类的初始化。(这里有个面试题,用Class.forName这个方法会导致初始化么?这个问题以后的博客中回答,暂时留个伏笔)
注意一定是重写findClass方法,切记切记。
双亲委托机制详细介绍
当一个类加载器调用了loadClass
之后,它并不会直接将其加载,而是先交给当前类加载器的父加载器直到最顶层的父加载器,然后再依次向下进行加载。
之前我们担心HelloWorld.class文件被系统类加载器,所以删除了HelloWorld文件,那有什么方法可以不删除HelloWorld文件使用MyClassLoader进行加载呢?
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) {
c = parent.loadClass(name, false);
} else {
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.
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();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- 从当前类加载器的加载缓存中根据类的全路径名查询是否存在该类,如果存在,则直接返回。
- 如果当前类存在父类加载器,则调用父类加载器的loadClass方法对其进行加载。
- 如果当前类加载器不存在父类加载器对该类进行加载
- 如果当前类的所有父类加载器都没有成功加载class,则尝试调用当前类加载器的findClass方法进行加载,该方法就是我们自定义加载器所需要重写的方法。
- 最后如果loadClass制定了resolve为false,所以不会进行连接阶段的继续执行,这也就解释了为什么通过类的加载器不会导致类的初始化。
看了源码之后,有两种方法可以在不删除HelloWorld文件,使用我们自定义的MyClassLoader进行加载。
- 直接将扩展类加载器作为MyClassLoader的父加载器
- 将MyClassLoader的父加载器设置为null
这个读者可以自行尝试,有更好的实现方案可以放到下方评论。