再谈类加载的父亲委托(Parent Delegation)机制
在父亲委托机制中,各个加载器按照父子关系形成了树状结构,除了根类加载器以外,其余的类加载器有且只有一个父加载器。
假设loader2的父亲为loader1,loader1的父亲为系统类加载器。假设Java程序要求loader2加载Sample类,代码如下:
Class sampleClass = loader2.loadClass("Sample");
loader2首先从自己的命名空间中查找Sample类是否已经加载,如果已经加载,就直接返回代表Sample类的Class对象的引用。
如果Sample类还没有被加载,loader2首先请求loader1代为加载,loader1再请求系统类加载器代为加载,系统类加载器再请求扩展类加载器代为加载,扩展类加载器再请求根类加载器代为加载。若根类加载器和扩展类加载器都不能加载,则系统类加载器尝试加载,若能加载成功,则将Sample类所对应的Class对象的引用返回给loader1,loader1再将引用返回给loader2,从而成功将Sample类加载进虚拟机。
若系统类加载器不能加载Smaple类,则loader1尝试加载Sample类,若loader1也不能成功加载,则loader2尝试加载。若所有的父加载器和loader2本身都不能加载,则抛出ClassNotFoundException异常。
父亲委托机制的优点?
父亲委托机制的优点是能够提高软件系统的安全性。因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。
例如,java.lang.Object类总是由根类加载器加载(BootStrap),其他任何用户自定义的类加载器都不可能加载含有恶意代码的。
java.lang.Object类。
有几个重要概念需要理解一下。(与安全有关)
1.命名空间
每个类加载器都有自己的命名空间,命名空间由加载器及所有父加载器所加载的类组成。
在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,
有可能会出现类的完整名字(包括类的包名)相同的两个类。
2.运行时包
由同一个类加载器加载的属于相同包的类组成运行时包。决定两个类是不是属于同一个运行时包,不仅
要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问可见(即默认访问级别)
的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。假设用户自己定义了一个类
java.lang.Spy,并由用户自定义的类加载器加载,由于jang.lang.Spy和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行
时包,所以java.lang.Spy不能访问核心类库java.lang包中的可见成员。
创建用户自定义的类加载器
要创建用户自己的类加载器,只需扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)即可,该方法根据参数指定的类的名字,返回对应的Class对象的引用。
package com.jfans;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
//给类加载器指定一个名字,在本例中只是为了便于区分不同的加载器对象
private String name;
private String path = "d://";
private final String fileType = ".class";
public MyClassLoader(String name) {
super();
this.name = name;
}
public MyClassLoader(ClassLoader parent,String name) {
super(parent);
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString(){
return this.name;
}
//自定义私有方法
private byte[] loadClassData(String name) throws ClassNotFoundException{
FileInputStream fis = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try{
//把name字符串中的"."替换为"/",从而把类中的包名转变为路径名
//例如,如果name为com.jfans.Sample,那么将转变为"com/jfans/Sample"
name = name.replaceAll("//.","////");
fis = new FileInputStream(new File(path + name + fileType));
baos = new ByteArrayOutputStream();
int ch = 0;
while( (ch=fis.read()) != -1){
baos.write(ch);
}
data = baos.toByteArray();
}catch(IOException e){
//System.out.println("exception e: " + e);
//异常转换
throw new ClassNotFoundException("class is not found:"+name,e);
}finally{
try{
if(fis != null){
fis.close();}
if(baos != null){
baos.close();}
}catch(IOException ioe){
ioe.printStackTrace();
}
}
return data;
}
//自定义加载器要重写的方法
protected Class findClass(String name) throws ClassNotFoundException{
byte[] data = loadClassData(name);
return defineClass(name,data, 0, data.length);
}
}
测试类:
package com.jfans;
public class MyClassLoaderTest {
public static void main(String[] args) {
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("D:/myapp/serverlib/");
MyClassLoader loader2 = new MyClassLoader(loader1,"loader2");
loader2.setPath("D:/myapp/clientlib/");
MyClassLoader loader3 = new MyClassLoader(null,"loader3");
loader3.setPath("D:/myapp/otherlib/");
test(loader1);
test(loader2);
test(loader3);
}
private static void test(ClassLoader classLoader){
try {
Class personClass = classLoader.loadClass("com.jfans.Person");
Object obj = personClass.newInstance();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//Person类
package com.jfans;
public class Person {
private String name;
public Person(){
this.name = "cuser";//default value
System.err.println("Person is load by: " + this.getClass().getClassLoader());//由此观察打印结果
}
public Personx(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试方法:可以将Person类分别放到上面三个加载器(loader1,loader2,loader3所指定的目录(path)下面。通过不同的组合,删除等,进行测试。相信能观察到Person到底由哪个加载器加载的,同时什么情况下会出现ClassNotFoundException。
测试前,阅读代码,记住父亲委托机制就可以了。
不同类加载器的命名空间存在以下关系:
●同一个命名空间内的类是相互可见的
●子加载器的命名空间包含所有父加载器的命名空间。因此子加载器加载的类能够看见父加载器加载的类。例如系统类加载器加载的类能够看见根类加载器加载的类。
●由父加载器加载的类不能看见子加载器加载的类。
●如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
所谓类A能看见类B,就是批类A的程序代码中可以引用类B的名字。例如:
class A{
B b = new B();
}
URLClassLoader类
在JDK的java.net包中,提供了一个功能比较强大的URLClassLoader类,它扩展了ClassLoader类。它不仅能从本地文件系统中加载类,还可以从网上下载类。Java程序可直接用URLClassLoader类作为用户自定义的类加载器。URLClassLoader类提供了以下形式的构造方法:
URL(URL[] urls) //父加载器为系统类加载器
URL(URL[] urls,ClassLoader parent) //parent参数指定父加载器
以上构造方法中的参数urls用来存放所有的URL路径,URLClassLoader将从这些路径中加载类。
类的卸载
当Sample类被加载、连接和初始化后,它的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。这是一个较大的话题。
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期内,始终不会被卸载。前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器,扩展类加载器,系统类加载器,Java虚拟机本身会始终引用这些类加载空对空,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象是始终可触及的。