类的加载器就是负责类的加载职责,对于任意一个class,都需要由加载它的类加载器和这个类本身确立其在JVM中的唯一性,这也就是运行时包,任何一个对象的class在JVM中只存在唯一的一份,比如String.class、Object.class在堆内存以及方法区中肯定是唯一的,但是不能绝对地理解为我们自定义的类在JVM中同样也是这样
文章目录
1 JVM内置三大类加载器
JVM为我们提供了三大内置的类加载器,不同的类加载器负责将不同的类加载到JVM内存之中,并且它们之间严格遵守着父委托的机制。
JDK除了提供上述三大类内置类加载器之外,还允许开发人员进行类加载器的扩展,也就是自定义类加载器,很多开源项目借助于自定义类加载器开发出了很多伟大的系统,比如OSGI、Tomcat的容器隔离等
1.1 Bootstrap类加载器介绍(根类加载器)
根加载器又称为Bootstrap类加载器,该类加载器是最为顶层的加载器,其没有任何父加载器,它是由C++编写的,主要负责虚拟机核心类库的加载,比如整个java.lang包都是由根加载器所加载的,可以通过-Xbootclasspath来指定根加载器的路径,也可以通过系统属性来得知当前JVM的根加载器都加载了哪些资源:
public static void main(String[] args) {
// null
System.out.println("BootStrapClassLoader: " + String.class.getClassLoader());
}
BootStrapClassLoader: null
String.class的类加载器是根加载器,根加载器是获取不到引用的,因此输出为null;
根加载器所在的加载路径可以通过sun.boot.class.path这个系统属性来获得
public static void main(String[] args) {
System.out.println(System.getProperty("sun.boot.class.path"));
}
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\lib\resources.jar;
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\lib\rt.jar;
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\lib\sunrsasign.jar;
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\lib\jsse.jar;
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\lib\jce.jar;
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\lib\charsets.jar;
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\lib\jfr.jar;
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\classes
1.2 ExtClassLoader(扩展类加载器)介绍
扩展类加载器的父加载器是根加载器,它主要用于加载JAVA_HOME下的jre\lb\ext子目录里面的类库。扩展类加载器是由纯Java语言实现的,它是java.lang.URLClassLoader的子类,它的完整类名是sun.misc.Launcher$ExtClassLoader。 扩展类加载器所加载的类库可以通过系统属性java.ext.dirs获得
public static void main(String[] args) {
System.out.println(System.getProperty("java.ext.dirs"));
}
C:\Program Files\AdoptOpenJDK\jdk-8.0.275.1-hotspot\jre\lib\ext;
C:\WINDOWS\Sun\Java\lib\ext
当然你也可以将自己的类打包成jar包,放到扩展类加载器所在的路径中,扩展类加载器会负责加载你所需要的类
1.3 ApplicationClassLoader(系统类加载器)介绍
系统类加载器是一种常见的类加载器,**其负责加载classpath下的类库资源。我们在进行项目开发的时候引入的第三方jar包,**系统类加载器的父加载器是扩展类加载器,同时它也是自定义类加载器的默认父加载器,系统类加载器的加载路径一般通过-classpath或者-cp指定,同样也可以通过系统属性java.class.path进行获取
public static void main(String[] args) {
System.out.println(System.getProperty("java.class.path"));
}
2 自定义类加载器
所有的自定义类加载器都是ClassLoader的直接子类或者间接子类,java.lang.ClassLoader是一个抽象类,它里面并没有抽象方法,但是有findClass方法,务必实现该方法,否则将会抛出Class找不到的异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
2.1 Hello World 程序
定义一个简单的ClassLoader,然后使用该类加载器加载一个简单的类
public class MyClassLoader extends ClassLoader {
/**
* 定义默认加载class文件的目录
*/
private final static Path DEFAULT_CLASS_DIR = Paths.get(System.getProperty("user.home"), "MyClassLoader");
private final Path classDir;
public MyClassLoader() {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
public MyClassLoader(String classDir) {
super();
this.classDir = Paths.get(classDir);
}
/**
* 指定class路径的同时,指定父类加载器
*
* @param classDir
* @param parent
*/
public MyClassLoader(String classDir, ClassLoader parent) {
super(parent);
this.classDir = Paths.get(classDir);
}
/**
* 重写findClass方法
*
* @param name: 类的全类名:包名 + 类名
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 将class文件读入内存
byte[] classByte = this.readClassByte(name);
// 如果数据为null,或者没有读到任何信息,则抛出ClassNotFoundException异常
if (null == classByte || classByte.length == 0) {
throw new ClassNotFoundException("Can not load the class " + name);
}
// 调用defineClass方法定义class
return this.defineClass(name, classByte, 0, classByte.length);
}
/**
* 读取clas文件
*
* @param name
* @return
*/
private byte[] readClassByte(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 find");
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
Files.copy(classFullPath, baos);
return baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("load the class " + name + " occur error.", e);
}
}
public static void main(String[] args) {
System.out.println();
}
@Override
public String toString() {
return "My ClassLoader";
}
}
至此我们完成了一个非常简单的基于磁盘的ClassLoader,几个关键的地方都已经做了标注,第一个构造函数使用默认的文件路径,第二个构造函数允许外部指定一个特定的磁盘目录,第三个构造函数除了可以指定磁盘目录以外还可以指定该类加载器的父加载器。
在我们定义的类加载器中,通过将类的全名称转换成文件的全路径重写findClass方法,然后读取class文件的字节流数据,最后使用ClassLoader的defineClass方法对class完成了定义。
开始使用我们定义的ClassLoader之前,有几个地方需要特别强调一下。第一,关于类的全路径格式,一般情况下我们的类都是类似于java.lang.String这样的格式,但是有时候不排除内部类,匿名内部类等;全路径格式有如下几种情况:
- 包名.类名,比如java.lang.String
- 包名.类名 内 部 类 , 比 如 j a v a x . s w i n g . J S p i n n e r 内部类,比如javax.swing.JSpinner 内部类,比如javax.swing.JSpinnerDefaultEditor
- 包名.类名 内 部 类 内部类 内部类内部类 匿 名 内 部 类 , 比 如 j a v a . s e c u r i t y . K e y S t o r e 匿名内部类,比如java.security.KeyStore 匿名内部类,比如java.security.KeyStoreBuilder$FileBuilder$1
- 包名.类名 匿 名 内 部 类 匿名内部类 匿名内部类匿名内部类,比如java.net.URLClassLoader$3$1
第二个需要强调的是defineClass方法,该方法的完整方法描述是defineClass(String name,byte[]b,int off,int len),其中,第一个是要定义类的名字,一般与findClass方法中的类名保持一致即可;第二个是class文件的二进制字节数组,这个也不难理解;第三个是字节数组的偏移量;第四个是从偏移量开始读取多长的byte数据。
大家思考一下,在类的加载过程中,第一个阶段的加载主要是获取class的字节流信息,那么我们将整个字节流信息交给defineClass方法不就行了吗,为什么还要画蛇添足地指定偏移量和读取长度呢?原因是因为class字节数组不一定是从一个class文件中获得的,有可能是来自网络的,也有可能是用编程的方式写入的,由此可见,一个字节数组中很有可能存储多个class的字节信息。
2.2 测试:
- 准备一个类:
package study.wyy.thread.jvm;
public class HelloWorld {
public String welcome() {
return "Hello World";
}
static {
System.out.println("Hello World Class is Initialized.");
}
}
-
javac 编译这个文件
-
将这个文件放到要自定义类加载器加载的目录,注意建文件目录
-
测试
需要保证你测试类的classpath下没有study.wyy.thread.jvm.HelloWorld,负责会被ApplicationClassLoader先加载这个类:原因就是
双亲委派机制
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassLoader classLoader = new MyClassLoader();
// 加载类
Class<?> aClass = classLoader.loadClass("study.wyy.thread.jvm.HelloWorld");
System.out.println("类加载器:" + aClass.getClassLoader());
// 通过反射new 实例
Object helloWorld = aClass.newInstance();
// 反射调用welcome方法
Method welcomeMethod = aClass.getMethod("welcome");
String result = (String) welcomeMethod.invoke(helloWorld);
System.out.println(result);
}
输出:
类加载器:My ClassLoader
Hello World Class is Initialized.
Hello World
注释掉下newInstance后面的代码,只使用MyClassLoader加载类
```java
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassLoader classLoader = new MyClassLoader();
// 加载类
Class<?> aClass = classLoader.loadClass("study.wyy.thread.jvm.HelloWorld");
}
是不会输出是HelloWorld的静态代码块的,,那是因为使用类加载器loadClass并不会导致类的主动初始化,它只是执行了加载过程中的加载阶段而已