类加载器的概念
一个类加载器是一个负责加载class类的对象,是一个抽象类。给定了一个类的二进制名字,类加载器应该尝试定位或者生成数据来构成一个类的定义。一个典型的策略就是将代码中的二进制名字转变成文件名然后从文件系统中读这个名字包含的class文件。
—来自于ClassLoader类的Document文档翻译
类加载的双亲委托机制
在父亲委托机制(也叫双亲委托机制)中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父类加载器。
下图说明了类加载器的过程
- 自底向上检查类是否已经加载
- 自顶向下尝试加载类
定义类加载器和初始类加载器
- 若有一个类加载器能够成功加载Test类,这个加载器叫做定义类加载器
- 所有能成功返回Class对象引用的,包括定义类加载器,都称为初始类加载器
以下例子证明了类加载器之间的逻辑关系:
public class MyTest13 {
public static void main(String[] args) {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
System.out.println(classLoader);
while (null != classLoader) {
classLoader = classLoader.getParent();
System.out.println(classLoader);
}
}
}
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null
自定义的第一个类加载器
package com.jvm.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* 创建了一个自己定义的类加载器,该例子用的是系统默认的AppClassLoader做为其双亲
* 用自己定义的方法来加载类,生成类的实例
*/
public class MyFirstClassLoader extends ClassLoader {
private final String extSuffix = ".class";
private String classloaderName;
/**
* ClassLoader的doc文档中说明 Each instance of ClassLoader has an associated parent class loader.
* 翻译:每个ClassLoader的实例都会有一个与之关联的父类加载器。
*/
public MyFirstClassLoader(String classloaderName) {
// super();可加可不加,会自动调用父类不带参数的构造方法
// 加上是为了提醒自己这边用的是系统类加载器(引用类加载器)作为MyFirstClassLoader的双亲;
super();
this.classloaderName = classloaderName;
}
public MyFirstClassLoader(String classloaderName, ClassLoader classLoader) {
// 这边是用自己定义的类加载器作为MyFirstClassLoader的双亲
super(classLoader);
this.classloaderName = classloaderName;
}
/**
* ClassLoader类的loadClass方法里会调用findClass方法来加载这个类。
*
* @param name 类的二进制名字(ClassLoader类的doc中有解释binary name。类似于java.lang.String)
* @return 该name所对应的类的Class对象
*/
@Override
public Class<?> findClass(String name) {
System.out.println("findClass invoked: " + className);
System.out.println("class loader name: " + this.classloaderName);
byte[] data = loadClassData(name);
// 调用父类的defineClass方法来返回该类对应的Class对象
return defineClass(name, data, 0, data.length);
}
/**
* 用自己定义的方法来加载类的二进制文件
*
* @param name 类的二进制名字(ClassLoader类的doc中有解释binary name。类似于java.lang.String)
* @return 加载后的二进制数组
*/
public byte[] loadClassData(String name) {
InputStream ins = null;
ByteArrayOutputStream baos = null;
byte[] data = null;
try {
this.classloaderName = this.classloaderName.replace(".", "/");
ins = new FileInputStream(new File(name + this.extSuffix));
baos = new ByteArrayOutputStream();
int ch = 0;
while ((ch = ins.read()) != -1) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
ins.close();
baos.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyFirstClassLoader classLoader = new MyFirstClassLoader("loader1");
classLoader.test(classLoader);
}
/**
* 测试
*
* @param classLoader 类加载器
*/
public void test(ClassLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("com.ssy.jvm.classloader.MyTest1");
System.out.println(clazz.newInstance());
System.out.println(clazz.getClassLoader());
}
}
结果
com.ssy.jvm.classloader.MyTest1@610455d6
sun.misc.Launcher$AppClassLoader@18b4aac2
解析
惊奇的发现并没有调用findClass方法!而且该类对应的classLoader竟然是AppClassLoader!why?
上述代码所描述的类加载器,默认的父加载器是系统类加载器。根据类加载器的双亲委托机制,当加载MyTest1的时候,一定不是自己加载,而是委托它的父亲(系统类加载器)去加载。系统类加载器可以加载这个类,所以就会被系统类加载器所加载。所以系统类加载器是MyTest1的定义类加载器,而系统类加载器和MyFirstClassLoader类加载器都是它的初始类加载器。
下面我们对上述例子做个改进
package com.ssy.jvm.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* 创建了一个自己定义的类加载器,该例子用的是系统默认的AppClassLoader做为其双亲
* 用自己定义的方法来加载类,生成类的实例
*/
public class MyFirstClassLoader extends ClassLoader {
private final String extSuffix = ".class";
private String classloaderName;
private String path;
/**
* ClassLoader的doc文档中说明 Each instance of ClassLoader has an associated parent class loader.
* 翻译:每个ClassLoader的实例都会有一个与之关联的父类加载器。
*/
public MyFirstClassLoader(String classloaderName) {
// super();可加可不加,会自动调用父类不带参数的构造方法
// 加上是为了提醒自己这边用的是系统类加载器(引用类加载器)作为MyFirstClassLoader的双亲;
super();
this.classloaderName = classloaderName;
}
public MyFirstClassLoader(String classloaderName, ClassLoader classLoader) {
// 这边是用自己定义的类加载器作为MyFirstClassLoader的双亲
super(classLoader);
this.classloaderName = classloaderName;
}
public void setPath(String path) {
this.path = path;
}
/**
* ClassLoader类的loadClass方法里会调用findClass方法来加载这个类。
*
* @param className 类的二进制名字(ClassLoader类的doc中有解释binary name。类似于java.lang.String)
* @return 该name所对应的类的Class对象
*/
@Override
protected Class<?> findClass(String className) {
System.out.println("findClass invoked: " + className);
System.out.println("class loader name: " + this.classloaderName);
byte[] data = loadClassData(className);
// 调用父类的defineClass方法来返回该类对应的Class对象
return this.defineClass(className, data, 0, data.length);
}
/**
* 用自己定义的方法来加载类的二进制文件
*
* @param className 类的二进制名字(ClassLoader类的doc中有解释binary className。类似于java.lang.String)
* @return 加载后的二进制数组
*/
public byte[] loadClassData(String className) {
InputStream is = null;
ByteArrayOutputStream baos = null;
byte[] data = null;
// mac系统换成 /
className = className.replace(".", "/");
try {
is = new FileInputStream(new File(this.path + className + this.extSuffix));
baos = new ByteArrayOutputStream();
int ch;
while ((ch = is.read()) != -1) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyFirstClassLoader loader1 = new MyFirstClassLoader("loader1");
loader1.setPath("/Users/ddcc/IdeaProjects/jvm_lecture/out/production/classes/");
Class<?> clazz = loader1.loadClass("com.ssy.jvm.classloader.MyTest1");
System.out.println("class: " + clazz.hashCode());
System.out.println(clazz.newInstance());
System.out.println(clazz.getClassLoader());
}
}
结果
class: 1627674070
com.ssy.jvm.classloader.MyTest1@511d50c0
sun.misc.Launcher$AppClassLoader@18b4aac2
分析
因为我们指定的路径还是系统类加载器默认的类编译路径,所以并没有成功,还是系统类加载器进行加载的
我们继续更改代码
我们把/Users/ddcc/IdeaProjects/jvm_lecture/out/production/classes/这个路径下的MyTest1.class文件删除(这一步很重要,不然系统类加载器还是能找到MyTest1.class文件),将MyTest1.class文件放置在桌面上
public static void main(String[] args) throws Exception {
MyFirstClassLoader loader1 = new MyFirstClassLoader("loader1");
loader1.setPath("/Users/ddcc/Desktop/");
Class<?> clazz = loader1.loadClass("com.ssy.jvm.classloader.MyTest1");
System.out.println("class: " + clazz.hashCode());
System.out.println(clazz.newInstance());
System.out.println(clazz.getClassLoader());
结果
class loader name: loader1
class: 1625635731
com.ssy.jvm.classloader.MyTest1@5e2de80c
com.ssy.jvm.classloader.MyFirstClassLoader@610455d6
分析
终于调用了我们自己定义的类加载器啦~然鹅
继续更改代码(前提将MyTest1.class文件删除)
public static void main(String[] args) throws Exception {
MyFirstClassLoader loader1 = new MyFirstClassLoader("loader1");
loader1.setPath("/Users/ddcc/Desktop/");
Class<?> clazz = loader1.loadClass("com.ssy.jvm.classloader.MyTest1");
System.out.println("class: " + clazz.hashCode());
System.out.println(clazz.newInstance());
System.out.println();
MyFirstClassLoader loader2 = new MyFirstClassLoader("loader2");
loader2.setPath("/Users/ddcc/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.ssy.jvm.classloader.MyTest1");
System.out.println("class : " + clazz2.hashCode());
System.out.println(clazz2.newInstance());
System.out.println();
}
结果
findClass invoked: com.ssy.jvm.classloader.MyTest1
class loader name: loader1
class: 1625635731
com.ssy.jvm.classloader.MyTest1@5e2de80c
findClass invoked: com.ssy.jvm.classloader.MyTest1
class loader name: loader2
class : 1872034366
com.ssy.jvm.classloader.MyTest1@5e481248
分析
发现两个类加载器都加载了MyTest1.class文件!而且,产生的Class类对应的hashcode值竟然不一样!(这说明这两个类加载器加载对应的Class对象不是同一个)。这与我们之前说过的:同一个类只会加载一次的理论是相悖的嘛?
其实不矛盾。这里面涉及到类加载器的命名空间概念
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类