1.内置的三大类加载器
JVM为我们提供了三大内置的类加载器,不同的类加载器负责将不同的类加载到JVM内存之中,并且它们之间严格遵守着父委托的机制,如图所示。
1.1 根类加载器介绍
根加载器又称为Bootstrap类加载器,该类加载器是最为顶层的加载器,其没有任何父加载器,它是由C++编写的,主要负责虚拟机核心类库的加载,比如整个java.lang包都是由根加载器所加载的,可以通过-Xbootclasspath来指定根加载器的路径, 也可以通过系统属性来得知当前JVM的根加载器都加载了哪些资源,示例代码如所示。
public class BootStrapClassLoader {
public static void main(String[] args) {
System.out.println("Bootstrap:" + String.class.getClassLoader());
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
Bootstrap:null
/Java/JavaVirtualMachines/adopt-openjdk-1.8.0_292/Contents/Home/jre/lib/resources.jar:
/Java/JavaVirtualMachines/adopt-openjdk-1.8.0_292/Contents/Home/jre/lib/rt.jar:
/Java/JavaVirtualMachines/adopt-openjdk-1.8.0_292/Contents/Home/jre/lib/sunrsasign.jar:
/Java/JavaVirtualMachines/adopt-openjdk-1.8.0_292/Contents/Home/jre/lib/jsse.jar:
/Java/JavaVirtualMachines/adopt-openjdk-1.8.0_292/Contents/Home/jre/lib/jce.jar:
/Java/JavaVirtualMachines/adopt-openjdk-1.8.0_292/Contents/Home/jre/lib/charsets.jar:
/Java/JavaVirtualMachines/adopt-openjdk-1.8.0_292/Contents/Home/jre/lib/jfr.jar:
/Java/JavaVirtualMachines/adopt-openjdk-1.8.0_292/Contents/Home/jre/classes
1.2 扩展类加载器介绍
扩展类加载器的父加载器是根加载器,它主要用于加载JAVA_HOME下的jre\lb\ext子目录里面的类库。扩展类加载器是由纯Java语言实现的, 它是java.lang.URLClassLoader的子类,它的完整类名是sun.misc.Launcher$Ext ClassLoader。扩展类加载器所加载的类库可以通过系统属性java.ext.dirs获得, 示例代码如所示。
public class ExtClassLoader {
public static void main(String[] args) {
System.out.println(System.getProperty("java.ext.dirs"));
}
}
jre/lib/ext
Java/Extensions
1.3 系统类加载器介绍
系统类加载器是一种常见的类加载器,其负责加载classpath下的类库资源。我们在进行项目开发的时候引入的第三方jar包,系统类加载器的父加载器是扩展类加载器,同时它也是自定义类加载器的默认父加载器,系统类加载器的加载路径一般通过-classpath或者-cp指定, 同样也可以通过系统属性java.class.path进行获取,示例代码如所示。
public class ApplicationClassLoader {
public static void main(String[] args) {
System.out.println(System.getProperty("java.class.path"));
System.out.println(ApplicationClassLoader.class.getClassLoader());
}
}
2.自定义类加载器
在本节中,我们开始用程序实现自定义的类加载器,所有的自定义类加载器都是ClassLoader的直接子类或者间接子类,java.lang.ClassLoader是一个抽象类,它里面并没有抽象方法,但是有findClass方法,务必实现该方法,否则将会抛出Class找不到的异常,示例代码如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
2.1 自定义类加载器,问候世界
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 {
// 定义默认的class存放路径
private final static Path DEFAULT_CLASS_DIR= Paths.get("","");
private final Path classDir;
// 使用默认的class路径
public MyClassLoader() {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
// 允许传入指定路径的class路径
public MyClassLoader(String classdir) {
super();
this.classDir=Paths.get(classdir);
}
// 指定class路径的同时,指定父类加载器
public MyClassLoader(String classDir, ClassLoader parent) {
super(parent);
this.classDir = Paths.get(classDir);
}
// 重写父类的findClass方法,这是至关重要的步骤
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 读取class的二进制数据
byte[] classBytes = this.readClassBytes(name);
// 如果数据为null,或者没有读到任务信息, 则跑出ClassNotFountException
if ( null == classBytes || classBytes.length == 0) {
throw new ClassNotFoundException("Can not load the class " + name);
}
// 调用defineClass方法定义class
return this.defineClass(name, classBytes, 0, classBytes.length);
}
// 将class文件读入内存
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("The class" + name + " not found.");
try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Files.copy(classFullPath, baos);
return baos.toByteArray();
}catch (IOException e) {
throw new ClassNotFoundException("load the class " + name + " occurerror.", e);
}
}
@Override
public String toString() {
return "MyClassLoader{" +
"classDir=" + classDir +
'}';
}
}
至此我们完成了一个非常简单的基于磁盘的ClassLoader, 几个关键的地方都已经做了标注,第一个构造函数使用默认的文件路径,第二个构造函数允许外部指定一个特定的磁盘目录,第三个构造函数除了可以指定磁盘目录以外还可以指定该类加载器的父加载器。
在开始使用我们定义的ClassLoader之前, 有几个地方需要特别强调一下。
第一, 关于类的全路径格式,一般情况下我们的类都是类似于java.lang.String这样的格式,但是有时候不排除内部类,匿名内部类等;全路径格式有如下几种情况。
java.lang.String:包名.类名
javax.swing.J SpinnerS Default Editor: 包名.类名$内部类
java.security.KeyStore$Builder$File Builder$1:包名.类名$内部类$内部类$匿名内部类
java.net.URL ClassLoader$3$1:包名.类名$匿名内部类$匿名内部类
第二个需要强调的是defineClass方法,该方法的完整方法描述是defineClass(String name,byte[] b, int off, intl en) ,其中,
- 第一个是要定义类的名字,一般与find Class方法中的类名保持一致即可;
- 第二个是class文件的二进制字节数组, 这个也不难理解;
- 第三个是字节数组的偏移量;
- 第四个是从偏移量开始读取多长的byte数据。
大家思考一下, 在类的加载过程中, 第一个阶段的加载主要是获取class的字节流信息, 那么我们将整个字节流
信息交给defineClass方法不就行了吗, 为什么还要画蛇添足地指定偏移量和读取长度呢?原因是因为class字节数组不一定是从一个class文件中获得的, 有可能是来自网络的, 也有可能是用编程的方式写入的, 由此可见, 一个字节数组中很有可能存储多个class的字节信息。
public class HelloWorld {
static {
System.out.println("Hello World Class is Initialized.");
}
public String welcome() {
return "Hello World";
}
}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MyClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
// 声明一个MyClassLoader
MyClassLoader classLoader = new MyClassLoader();
// 使用MyClassLoader加载HelloWorld
Class<?> aClass = classLoader.loadClass("chapter10.HelloWorld");
System.out.println(aClass.getClassLoader());
// 注释
Object helloWorld = aClass.newInstance();
System.out.println(helloWorld);
Method welcomeMethod = aClass.getMethod("welcome");
String result = (String) welcomeMethod.invoke(helloWorld);
System.out.println("Result:" + result);
}
}
2.2 双亲委托机制详细介绍
父委托机制:当一个类加载器被调用了loadClass之后, 它并不会直接将其加载,而是先交给当前类加载器的父加载器尝试加载直到最顶层的父加载器, 然后再依次向下进行加载,下图显示了类加载器的委托流程。
2.3 破坏双亲委托机制
2.4 类加载器命名空间、运行时包、类的卸载等
2.4.1.类加载器命名空间
每一个类加载器实例都有各自的命名空间,命名空间是由该加载器及其所有父加载器所构成的, 因此在每个类加载器中同一个class都是独一无二的, 类加载器命令空间代码如所示。
public class NameSpace {
public static void main(String[] args) throws ClassNotFoundException {
// 获取系统类加载器
ClassLoader classLoader = NameSpace.class.getClassLoader();
Class<?> aclass = classLoader.loadClass("chapter10.HelloWorld");
Class<?> bclass = classLoader.loadClass("chapter10.HelloWorld");
System.out.println(aclass.hashCode());
System.out.println(bclass.hashCode());
System.out.println(aclass==bclass);
}
}
运行上面的代码,不论load多少次HelloWorld,你都将会发现他们始终是同一份class对象。类被加载后的内存情况如图所示。
但是,使用不同的类加载器,或者同一个类加载器的不同实例,去加载同一个class,则会在堆内存和方法区产生多个class的对象。
(1)不同类加载器加载同一个class
(2)相同类加载器加载同一个class
在类加载器进行类加载的时候,首先会到加载记录表也就是缓存中,查看该类是否已经被加载过了,如果已经被加载 过了,就不会重复加载,否则将会认为其是首次加载,图10-4是同一个class被不同类加载器加载之后的内存情况。
2.4.2 运行时包
我们在编写代码的时候通常会给一个类指定一个包名,包的作用是为了组织类,防止不同包下同样名称的class引起冲突, 还能起到封装的作用, 包名和类名构成了类的全限定名称。在JVM运行时class会有一个运行时包, 运行时的包是由类加载器的命名空间和类的全限定名称共同组成的, 比如Test的运行时包如下所示:
BootstrapClassLoader.ExtclassLoader.AppClassloader.MyclassLoader.com.concurrent.chapter10.Test
2.4.3 初始化类加载器
每一个类在经过ClassLoader的加载之后, 在虚拟机中都会有对应的Class实例, 如果某个类C被类加载器CL加载, 那么CL就被称为C的初始类加载器。JVM为每一个类加载器维护了一个列表, 该列表中记录了将该类加载器作为初始类加载器的所有class, 在加载一个类时, JVM使用这些列表来判断该类是否已经被加载过了, 是否需要首次加载。
根据JVM规范的规定,在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始类加载器,比如java.lang.String首先经过了BrokerDelegateClassLoader类加载器,依次又经过了系统类加载器、扩展类加载器、根类加载器,这些类加载器都是java.lang.String的初始类加载器,JVM会在每一个类加载器维护的列表中添加该class类型, 如图10-5所示。
虽然Simple Class和java.lang.String由不同的类加载器加载, 但是在Broker DelegateClassLoader的class列表中维护了Simple Class.class和String.class, 因此在Simple Class中是可以正常访问rt.jar中的class的。
2.4.4 类的卸载
JVM规定了一个Class只有在满足下面三个条件的时候才会被GC回收, 也就是类被卸载。
- 该类所有的实例都已经被GC, 比如Simple.class的所有Simple实例都被回收掉。
- 加载该类的ClassLoader实例被回收。
- 该类的class实例没有在其他地方被引用。