前言
- 学习就应该阅读官方写JavaDoc,JavaDoc是第一手资料,应该是准确无疑的。
- 本篇以翻译ClassLoader类中文档注释为主,如有不通顺,请自行看英文注释。
英文版
翻译
- 类加载器是负责加载类的对象。 ClassLoader 类是一个抽象类。 给定类的二进制名称(binary name),类加载器应该尝试定位或生成(locate or generate)构成类定义的数据。 典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
批注:
- 给定类的名称,类加载器会找到定义对应类的数据.
- binary name: 其实就是字符串
- "java.lang.String"
- "javax.swing.JSpinner
$DefaultEditor"($后面跟着是内部类的类名)- "java.security.KeyStore
$Builder$FileBuilder$1"($1类中的第一个匿名内部类)- "java.net.URLClassLoader
$3$1"(URLClassLoader的第3个匿名内部类中第一个匿名内部类)- 定位或生成(locate or generate): 说定位是因为类存在磁盘的某个地方等待被加载,说生成是因为有些类是动态生成的,如动态代理。
- 每个Class对象都包含定义它的ClassLoader的reference(引用)。
批注:在Class类中有成员变量ClassLoader,所以Class类有getClassLoader()方法。
- 数组类的类对象不是由类加载器创建的,而是根据 Java 运行时的需求自动创建的。 Class.getClassLoader()返回的数组类的类加载器与其元素类型的类加载器相同; 如果元素类型是原始类型,则数组类没有类加载器。
- 样例说明
public class ClassLoaderTest3 { public static void main(String[] args) { String[] strings = new String[2]; System.out.println(strings.getClass().getClassLoader());//元素类型是String,由BootStrap加载 System.out.println("---------------------"); ClassLoaderTest3[] classLoaderTest3s = new ClassLoaderTest3[2]; System.out.println(classLoaderTest3s.getClass().getClassLoader()); System.out.println("---------------------"); int[] a = new int[2]; System.out.println(a.getClass().getClassLoader()); } }- 结果
- 应用程序实现(继承)ClassLoader,用以扩展 Java 虚拟机动态加载类的方式。
批注:自定义类加载器的目的是扩展 Java 虚拟机动态加载类的方式,在默认情况是以双亲委托机制来加载类的,如果你不喜欢双亲委托机制,就自行继承ClassLoader,实现不一样的加载方式。
- 安全管理器通常可以使用类加载器来指示安全域。
批注:俺看不懂,自行百度。
- ClassLoader类使用委托模型来搜索类和资源。 ClassLoader 的每个实例都有一个关联的父类加载器。 当请求查找类或资源时, ClassLoader实例会将类或资源的搜索委托给其父类加载器,然后再尝试查找类或资源本身。 虚拟机的内置类加载器,称为“引导类加载器(Bootstrap ClassLoader)”,它本身没有父级,但可以作为ClassLoader实例的父级。
- 支持并发加载类的类加载器被称为具有并行能力的类加载器,并且需要在类初始化时通过调用ClassLoader.registerAsParallelCapable方法来注册自己,使其可以并行加载类。 请注意, ClassLoader类默认注册为具有并行能力。 但是,虽然它们具有并行能力,但它的子类仍然需要注册自己。
- 在委托模型不是严格分层的环境中,类加载器需要具有并行能力,否则类加载会导致死锁,因为加载器锁在类加载过程中一直保持着(参见loadClass方法)。
- 通常,Java 虚拟机以平台相关的方式从本地文件系统加载类。 例如,在 UNIX 系统上,虚拟机从CLASSPATH环境变量定义的目录中加载类。
- 但是,有些类可能不是来自文件; 它们可能来自其他来源,例如网络,或者它们可以由应用程序构建。 方法
defineClass将一个字节数组转换为Class 类的一个实例。 可以使用Class.newInstance创建这个新定义的类的实例。 - 类加载器创建的对象的方法和构造函数中有引用其他类。 为了确定所引用的类,Java 虚拟机调用最初创建类的类加载器的loadClass方法。
批注:说一个对象里面的方法有使用其它的类,如,method(){return new String("hehe");},引用了String类。
来一些代码来增加一下理解
自定义类加载器
- 在开始之前先提一下,上面的文档说了:
defineClass将一个字节数组转换为Class 类的一个实例。 可以使用Class.newInstance创建这个新定义的类的实例。defineClass参数- 再说了,需要让类加载器找到需要加载的类,它才能加载,上面提到的binary name就很关键。
- 这个自定义代码有些问题,先看看
public class UserClassLoader extends ClassLoader {
private String classLoaderName;
private final String SUFFIX = ".class";
private String path;
public void setPath(String path) {
this.path = path;
}
public UserClassLoader(String classLoaderName) {//构造方法
super();//以系统类加载器为该加载器的父加载器
this.classLoaderName = classLoaderName;
}
//构造方法,可以指定父类加载器的构造方法
public UserClassLoader(ClassLoader parent, String classLoaderName) {
super(parent);//显式定义该类加载器的父加载器
this.classLoaderName = classLoaderName;
}
@Override
public String toString() {
return "classLoaderName='" + classLoaderName;
}
@Override
protected Class<?> findClass(String className) {//重写
byte[] data = loaderClassData(className);
return defineClass(className,data,0,data.length);//字节数组变成Class
}
private byte[] loaderClassData(String className) {//将class文件变成字节数组
InputStream in = null;
ByteArrayOutputStream outputStream = null;
byte[] data = null;
try {
className = className.replace(".", "/");
in = new FileInputStream(path + className + SUFFIX);//文件输入流
outputStream = new ByteArrayOutputStream();//字节输出流
int ch;
while ((ch = in.read()) != -1) {
outputStream.write(ch);
}
data = outputStream.toByteArray();//字节数组
} catch (Exception e) {
e.printStackTrace();
} finally {
Optional.ofNullable(in).ifPresent(i -> {
try {
i.close();//关闭流
} catch (IOException e) {
e.printStackTrace();
}
});
Optional.ofNullable(outputStream).ifPresent(i -> {
try {
i.close();//关闭流
} catch (IOException e) {
e.printStackTrace();
}
});
}
return data;//返回字节数组。数组内容就是类
}
public static void main(String[] args) throws Exception {
UserClassLoader userClassLoader = new UserClassLoader("userClassLoader");
UserClassLoader.test(userClassLoader);
}
public static void test(ClassLoader classLoader) throws Exception {//测试
//指定要加载的class文件,不能是当前类,类名是binary name
Class<?> clazz = classLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");
Object object = clazz.newInstance();//生成类的实例
System.out.println(object);//打印
}
}
- 结果,毫无疑问就是类名
那么问题来了,上面自定义的类加载器有被使用吗
- 在上面的代码中,在findClass方法中添加一下打印语句
@Override protected Class<?> findClass(String className) { System.out.println("findClass invoke: "+className); System.out.println("classLoader name: "+classLoaderName); byte[] data = loaderClassData(className); return defineClass(className,data,0,data.length); } - 结果,加载类的时候并没有使用自定义类加载去加载。
- 再在test方法中打印一下类是由哪个加载器加载的
public static void test(ClassLoader classLoader) throws Exception { //加载类,不能是当前类 Class<?> clazz = classLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest"); Object object = clazz.newInstance(); System.out.println(object); System.out.println(object.getClass().getClassLoader()); - 结果,该类是由系统(App)类加载器加载的
为什么会出现自定义类加载器没有被使用的情况
- 可以看到在main方法里,实例化自定义类加载器时,用的是第一个构造方法。
public UserClassLoader(String classLoaderName) {//构造方法 super();//以系统类加载器为该加载器的父加载器 this.classLoaderName = classLoaderName; } - 根据双亲委托机制, 当使用自定义类加载器加载类时,它会委托其父类加载器(
AppClassLoader,即系统类加载器)去加载。所以,最终是系统类加载器加载的"com.jvmstudy.classloading.ClassLoaderTest"
更进一步
- 系统类加载器是加载当前类路径下的类,就说.java文件编译后的.class文件默认输出的路径
- 删除默认路径下加载的class文件。
-
main方法如下:
public static void main(String[] args) throws Exception { UserClassLoader userClassLoader = new UserClassLoader("userClassLoader"); userClassLoader.setPath("C:/Users/25852/Desktop/"); //不能是当前类 Class<?> clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest"); Object object = clazz.newInstance(); System.out.println("class HashCode: "+clazz.hashCode()); System.out.println(object.getClass().getClassLoader()); } -
结果:
-
原因何在?
- 根据双亲委托机制,还是自定义类加载器不会自己先加载类,但是,其父类加载器都无法加载类,根类加载器和扩展类加载器就不用说了肯定加载不了,系统类加载器也加载不了,因为默认路径中的class文件已经被删除了。
- 最终,都委托了一遍,就只能是自定义类自己加载了,我们已经告诉自定义类加载去桌面加载类了。
重新编译一下,将原本删除的class文件重新生成一下
- main方法改编如下:
public static void main(String[] args) throws Exception { UserClassLoader userClassLoader = new UserClassLoader("userClassLoader"); Class<?> clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 Object object = clazz.newInstance(); System.out.println("class HashCode: "+clazz.hashCode()); System.out.println(object.getClass().getClassLoader()); System.out.println("-----------------------"); UserClassLoader userClassLoader2 = new UserClassLoader("userClassLoader2"); Class<?> clazz1 = userClassLoader2.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 Object object1 = clazz1.newInstance(); System.out.println("class HashCode: "+clazz.hashCode()); System.out.println(object1.getClass().getClassLoader()); } - 结果:
- 说明同一个类只会被加载一次。
老样子,删掉默认路径下的class,将其移到桌面
- main方法改编如下:
public static void main(String[] args) throws Exception { UserClassLoader userClassLoader = new UserClassLoader("userClassLoader"); userClassLoader.setPath("C:/Users/25852/Desktop/"); Class<?> clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 Object object = clazz.newInstance(); System.out.println("class HashCode: "+clazz.hashCode()); System.out.println(object.getClass().getClassLoader()); System.out.println("-----------------------"); UserClassLoader userClassLoader1 = new UserClassLoader("userClassLoader1"); userClassLoader1.setPath("C:/Users/25852/Desktop/"); Class<?> clazz1 = userClassLoader1.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 Object object1 = clazz1.newInstance(); System.out.println("class HashCode: "+clazz1.hashCode()); System.out.println(object1.getClass().getClassLoader()); } - 结果:
- 前面才说同一个类只会被加载一次,这里怎么就不对了?
- 这就涉及到类加载器的命名空间问题了。
- 下亿篇博客再介绍命名空间问题。