类加载器
注意图中的类加载器之间并不是继承关系,而是包含关系。
根类加载器
根类加载器(BootstrapClassLoader):负责加载存放在JDK/jre/lib下的指定的jar,也可以使用-Xbootclasspath参数指定哪些jar要使用根类加载器加载。
下面的代码将会打印BootstrapClassLoader以及根类加载器负责加载哪些jar:
System.out.println("BootstrapClassLoader:" + String.class.getClassLoader());
Arrays.asList(System.getProperty("sun.boot.class.path").split(";")).stream().forEach(System.out::println);
运行结果如下所示,其中String.class的类加载器是根类加载器,根类加载器是获取不到引用的,因此输出为null,而根类加载器所加载的jar可以通过系统属性sun.boot.class.path获取。
BootstrapClassLoader:null
D:\Program Files\Java\jdk1.8.0_172\jre\lib\resources.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\sunrsasign.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\jce.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.8.0_172\jre\classes
要想直接使用根类加载器加载自定义的类,有以下几种方法:
- 将class文件放入上面打印的路径D:\Program Files\Java\jdk1.8.0_172\jre\classes中
- 将自定义的类压缩成jar,在运行时用-Xbootclasspath指定jar的路径
-Xbootclasspath的使用方法:
- -Xbootclasspath:完全取代基本核心的Java class搜索路径,否则要重新写所有Java核心class文件
- -Xbootclasspath/a:加在核心class文件搜索路径后面
- -Xbootclasspath/p:加在核心class文件搜索路径前面
扩展类加载器
扩展类加载器(Extension ClassLoader):负责加载JDK\jre\lib\ext目录中或者由java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器。扩展类加载器的父加载器是根类加载器,该加载器由sun.misc.Launcher$ExtClassLoader实现。
下面的代码将会打印ExtClassLoader以及扩展类加载器负责加载哪些目录的jar:
System.out.println("ExtClassLoader:" + com.sun.nio.zipfs.ZipDirectoryStream.class.getClassLoader());
System.out.println("ExtClassLoader parent:" + com.sun.nio.zipfs.ZipDirectoryStream.class.getClassLoader().getParent());
Arrays.asList(System.getProperty("java.ext.dirs").split(";")).stream().forEach(System.out :: println);
运行结果如下所示:
ExtClassLoader:sun.misc.Launcher$ExtClassLoader@2503dbd3
ExtClassLoader parent:null
D:\Program Files\Java\jdk1.8.0_172\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
系统类加载器
系统类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)所指定的类,系统类加载器的加载路径可以通过-classpath来指定,同样也可以通过系统属性java.class.path来获取。系统类加载器的父加载器是扩展类加载器,该类加载器由sun.misc.Launcher$AppClassLoader来实现。
下面的代码将会打印AppClassLoader以及系统类加载器负责加载哪些目录的class:
System.out.println("AppClassLoader:" +AppClassLoaderTest.class.getClassLoader());
System.out.println("AppClassLoader parent:" +AppClassLoaderTest.class.getClassLoader().getParent());
Arrays.asList(System.getProperty("java.class.path").split(";")).stream().forEach(System.out::println);
运行结果如下:
AppClassLoader:sun.misc.Launcher$AppClassLoader@2a139a55
AppClassLoader parent:sun.misc.Launcher$ExtClassLoader@7852e922
.
自定义加载器
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类,我们只需要重写findClass方法即可。
下面的代码自定了一个类加载器来加载磁盘上的class文件:
package com.morris.jvm.classloader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
// 自定义类加载器必须继承ClassLoader
public class MyClassLoader extends ClassLoader {
private static final Path DEFAULT_CLASS_DIR = Paths.get("D:","classloader");
private final Path classDir;
public MyClassLoader() {
this.classDir = DEFAULT_CLASS_DIR;
}
public MyClassLoader(ClassLoader parent) {
super(parent);
this.classDir = DEFAULT_CLASS_DIR;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = this.readByte(name);
if(null == bytes || 0 == bytes.length) {
throw new ClassNotFoundException("Can not load the class " + name);
}
return this.defineClass(name, bytes, 0, bytes.length);
}
// 将class文件读入内存
private byte[] readByte(String name) throws ClassNotFoundException{
String classPath = name.replace(".", "/");
Path classFullPath = this.classDir.resolve(classPath + ".class");
if(!classFullPath.toFile().exists()) {
throw new ClassNotFoundException("The class " + name + " not found.");
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
Files.copy(classFullPath, baos);
return baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("load the class " + name + " error", e);
}
}
}
下面写一个简单的HelloWorld程序,使用自定义类加载器对其进行加载。
package com.morris.jvm.classloader;
public class HelloWorld {
static {
System.out.println("HelloWorld Class is initialized.");
}
}
将HelloWorld类编译后将class文件复制到D:\classloader\com\morris\jvm\classloader目录下,同时将class path中的HelloWorld.class删除,如果使用的集成开发环境,则需要将HelloWorld.java一并删除,否则将会由系统类加载器加载。
使用下面的代码使用自定义的累加器尝试加载HelloWorld:
package com.morris.jvm.classloader;
public class MyClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> clazz = myClassLoader.loadClass("com.morris.jvm.classloader.HelloWorld");
System.out.println(clazz.getClassLoader());
}
}
运行结果如下,虽然HelloWorld类被成功加载并且输出了自定义类加载器的信息,但是HelloWorld类的静态代码块并没有输出,因为使用类加载器的loadClass并不会导致类的主动初始化,只是执行了类加载过程中的加载阶段而已。
com.morris.jvm.classloader.MyClassLoader@7f31245a
Class.forName()和ClassLoader.loadClass()区别
- Class.forName():除了将类的.class文件加载到jvm中之外,还会对类进行解析,执行类中的static块进行初始化;
- ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块进行初始化。
注:Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采会调用构造函数,创建类的对象。
双亲委托机制
如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载。双亲委托机制的好处就是避免有些类被重复加载。
源码分析如下:
摘自jdk1.8 java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查是否被加载过
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();
// 父类加载失败,则使用自己的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;
}
}
从上面对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:
- 如果不想打破双亲委派模型,那么只需要重写findClass方法即可
- 如果想打破双亲委派模型,那么就重写整个loadClass方法
更多精彩内容关注本人公众号:架构师升级之路