1.类加载器分类
java类加载器层次结构如下:
1)bootstrap classloader
引导(也称为原始)类加载器,它负责加载Java的核心类。这个加载器的是非常特殊的,它不是 java.lang.ClassLoader的子类,而是由JVM自身实现的。
加载的jar文件如下:
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/classes
2)extension classloader
扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的 类包。
本地测试输出的目录是:
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
3)system classloader
系统(也称为应用)类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或 者 CLASSPATH操作系统属性所指定的JAR类包和类路径。总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。
Launcher的源码中,首先初始化extension classloader,然后初始化system classloader,parent是extension classloader。将system classloader设置成当前线程的context classloader。
加载机制:
classloader 加载类用的是全盘负责委托机制。所谓全盘负责,即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入;委托机制则是先让parent(父)类加载器 (而不是super,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。此外类加载还采用了cache机制,也就是如果cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。
2.类加载机制应用
1)动态Java类的加载与重加载
双亲委托加载机制保证了java核心的类有jdk自己加载,但是class加载完成之后就不能重新加载,此时可能需要自定义类加载器进行重新加载。比如一些热部署的插件就会提供类似的功能。
思考一个问题:能不能写个java.lang.String的类并加载?答案是不能。
自定义我们的classloader:
public class MyClassLoader extends ClassLoader {
private String basedir; // 需要该类加载器直接加载的类文件的基目录
private HashSet dynaclazns; // 需要由该类加载器直接加载的类名
public MyClassLoader(String basedir, String[] clazns) {
super(null); // 指定父类加载器为 null
this.basedir = basedir;
dynaclazns = new HashSet();
loadClassByMe(clazns);
}
private void loadClassByMe(String[] clazns) {
for (int i = 0; i < clazns.length; i++) {
loadDirectly(clazns[i]);
dynaclazns.add(clazns[i]);
}
}
private Class loadDirectly(String name) {
Class cls = null;
StringBuffer sb = new StringBuffer(basedir);
String classname = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator + classname);
File classF = new File(sb.toString());
try {
cls = instantiateClass(name,new FileInputStream(classF),
classF.length());
} catch (IOException e) {
e.printStackTrace();
}
return cls;
}
private Class instantiateClass(String name, InputStream fin, long len) throws IOException {
byte[] raw = new byte[(int) len];
fin.read(raw);
fin.close();
return defineClass(name,raw,0,raw.length);
}
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if(!this.dynaclazns.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if (cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
}
测试类:
public class Main {
public static void main(String[] args) {
try {
MyClassLoader classLoader = new MyClassLoader("D:\\workspace_git\\classloader-test\\out\\production\\classloader-test\\",new String[]{"java.lang.String"});
Class stringClass = classLoader.loadClass("java.lang.String");
stringClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行输出:
java.lang.SecurityException: Prohibited package name: java.lang
查看源代码可知:
defineClass方法中调用preDefineClass对classname进行了检查不允许重写“java.”开头的类,所以无法加载自定义java.lang.String类。
2)自定义classloader实现类隔离
两个包名、类名完全相同的类也可能是不同的类,比如通过不同的classloader加载就不同。
利用这个特性可以实现框架中容器的隔离机制。试想使用一个框架的时候很可能引入的第三方库中的jar版本和框架本身jar版本冲突,如何解决呢。使用不同的类加载器分别加载框架使用的类以及应用本身使用的类即可。