1. 类加载器
类加载器用来把类加载到java虚拟机中。类的加载过程采用父亲委托机制,这种机制能更好地保证java平台的安全。除了java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当java程序请求加载器loader1加载某个类时,loader1会首先委托自己的父加载器去加载这个类,如果父加载器能加载,则由父加载器加载,否则才由loader1本身完成加载。
java虚拟机自带以下几种加载器:
- 根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等。根类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,是用C++编写的,并没有继承java.lang.ClassLoader类 。
- 扩展(Extension)类加载器:它的父加载器为根类加载器。它从java.ext.dirs系统属性指定的目录中加载类库,或者从JDK的安装目录的jre/lib/ext子目录下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯java类,是java.lang.ClassLoader类的子类。
- 系统(System)类加载器:也称为应用类加载器,它的父加载器是扩展类加载器。它从classpath环境变量或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯java类,是java.lang.ClassLoader类的子类。
除了以上虚拟机自带的加载器,用户还可以定制自己的类加载器。java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器应该继承ClassLoader类。
类加载器测试程序
public class Test332{
public static void main(String[] args) {
Class c;
ClassLoader cl,cl1;
cl=ClassLoader.getSystemClassLoader();
System.out.println(cl);
while(cl!=null)
{
cl1=cl;
cl=cl.getParent();
System.out.println(cl1+"'s parent is "+cl);
}
try{
c=Class.forName("java.lang.String");
cl=c.getClassLoader();
System.out.println("java.lang.String's classloader is "+cl);
c=Class.forName("Test332");
cl=c.getClassLoader();
System.out.println("Test332's classloader is "+cl);
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
输出结果为
sun.misc.Launcher$AppClassLoader@63c78e57
sun.misc.Launcher$AppClassLoader@63c78e57's parent is sun.misc.Launcher$ExtClassLoader@425224ee
sun.misc.Launcher$ExtClassLoader@425224ee's parent is null
java.lang.String's classloader is null
Test332's classloader is sun.misc.Launcher$AppClassLoader@63c78e57
2. 父亲委托机制
除了根类加载器,其余类加载器都有唯一的一个父类加载器。假设系统要加载Sample类,loader2会首先从自己的命名空间里查找Sample类是否已经被加载,如果已经加载,直接返回代表Sample类的Class对象的引用。如果Sample类还没被加载,loader2首先请求loader1加载,loader1再请求系统类加载器…直至请求根类加载器加载。如果根类加载器和扩展类加载器都不能加载Sample类,则系统类加载器尝试加载。如果可以加载,则将Sample类对应的Class对象引用返回给loader1,loader1再将引用返回给loader2,从而成功地将Sample类加载到虚拟机中。如果系统类不能加载,则loader尝试加载,如果loader1不能成功加载,则loader2尝试加载,如果所有类加载器都不能加载,则抛出ClassNotFoundException异常。
如果一个类加载器可以成功加载Sample类,这个类加载器被称为定义类加载器,所有能成功返回Class对象的引用类能加载器,包括定义类加载器,都被称为初始类加载器。上图中,如果loader1实际加载了Sample类,则loader1为Sample类的定义类加载器,loader1和loader2为Sample类的初始类加载器。
类加载器之间的父子关系指的是类加载器之间的包装关系,而不是类之间的继承关系。一对父子加载器可能是同一个加载器类的两个实例,也可能不是。在子加载器对象中包装了一个父加载器对象,如下代码:
ClassLoader cl1=new MyClassLoader();
ClassLoader cl2=new MyClassLoader(cl1);
cl1和cl2都是MyClassLoader的实例,cl2包装了cl1,因此cl1是cl2的父加载器。
父亲委托机制的优点:保证软件系统的安全性!在此机制下,用户自己编写的类加载器无法加载由父类加载器加载的可靠类,防止不可靠的代码代替由父加载器加载的可靠代码。
命名空间。每个类加载器都有命名空间,命名空间由该加载器和所有父加载器所加载的类组成。在同一个命名空间里,不会出现类的完整名称(包名+类名)完全相同的两个类。在不同命名空间里则有可能出现。
运行时包。由同一个类加载器加载的属于相同包的类组成了运行时包,决定两个类是否属于同一个运行时包,不仅要看包名是否相同,还要看定义类加载器是否一致。只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。这样做可以避免用户自定义的类冒充核心类库的类,访问核心类库的包可见成员。假设用户自定义了java.lang.Spy类,并由用户自定义的类加载器加载,但由于java.lang.Spy类和核心类库java.lang.*由不同加载器加载,属于不同运行时包,所以java.lang.Spy类不能访问核心类库java.lang.*的包可见成员。
3. 用户自定义类加载器
用户自定义类加载器时,只需要扩展java.lang.ClassLoader类,然后覆盖findClass(String name)方法,该方法根据参数制定的类名称,返回对应的Class对象的引用。
public class MyClassLoader extends ClassLoader {
private String name; //类加载器的名字
private String path="d:\\"; //加载器的路径
private final String fileType=".class";//class文件扩展名
public MyClassLoader(String name){
super();//让系统类加载器成为该加载器的父加载器
this.name=name;
}
public MyClassLoader(ClassLoader parent,String name){
super(parent);//显示指定该类加载器的父加载器
this.name=name;
}
@Override
public String toString() {
return this.name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
byte[] data=loadClassData(name);
return this.defineClass(name, data, 0,data.length);
}
private byte[] loadClassData(String name){
InputStream is=null;
byte[] data =null;
ByteArrayOutputStream baos=null;
this.name=this.name.replace(".", "\\");
try {
is=new FileInputStream(new File(path+name+fileType));
baos=new ByteArrayOutputStream();
int ch=0;
while ((ch=is.read())!=-1) {
baos.write(ch);
}
data=baos.toByteArray();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
try {
is.close();
baos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return data;
}
public static void main(String[] args) throws Exception{
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");//null指根类加载器,加载核心类库
loader3.setPath("d:\\myapp\\otherlib\\");
test(loader2);
test(loader3);
}
public static void test(ClassLoader loader) throws Exception{
Class clazz=loader.loadClass("com.jvm.classloader.Sample");
Object object=clazz.newInstance();
}
}
其中,用户自定义的loader1、loader2和loader3之间的关系如图所示。
其中,test()方法用来测试类加载器的用法,它调用ClassLoader类的loadClass()方法加载Sample类。编写Sample类和Dog类。
public class Sample {
public int v1=1;
public Sample(){
System.err.println("Sample is loaded by : "+ this.getClass().getClassLoader());
new Dog();
}
}
public class Dog {
public Dog(){
System.err.println("Dog is loaded by : "+ this.getClass().getClassLoader());
}
}
通过改变Sample类和Dog类的存储路径,来演示类加载器的特性。
Sample.class和Dog.class同时复制到d:\myapp\serverlib和d:\myapp\otherlib目录下。运行MyClassLoader类,打印结果为:
Sample is loaded by loader1
Dog is loaded by loader1
Sample is loaded by loader3
Dog is loaded by loader3
可以看出,在loader1和loader3的命名空间中都存在Sample类和Dog类,即在java虚拟机的方法区内,有两个Sample类和两个Dog类的二进制数据结构。
当Sample类中主动使用了Dog类,java虚拟机会使用Sample类的定义类加载器去加载Dog类,加载过程同样采用父亲委托机制。如果Dog类从d:\myapp\serverlib文件夹移动到d:\myapp\syslib文件夹中,Sample类仍然由loader1加载,但是Dog类被AppClassLoader加载。
不同类加载器的命名空间存在以下关系:
- 同一个命名空间里的类是相互可见的
- 子加载器的命名空间包含所有父加载器的命名空间。因此子加载器加载的类中可以看见父加载器加载的类。例如,系统类加载器加载的类能看到根加载器加载的类
- 父加载器加载的类不能看见子加载器加载的类
- 如果两个加载器之间没有直接或者间接的父子关系,则他们各自加载的类相互不可见
所谓类A能看见类B,就是指A的程序代码中能引用B类。如
class A{
B b=new B();
}
如果在MyClassLoader类中的main()函数内容如下:
public static void main(String[] args)throws Exception
{
MyClassLoader loader1=new MyClassLoader("loader1");//父加载器为系统类加载器
loader1.setPath("D:\\myapp\\serverlib\\");
Class objClass=loader1.loadClass("Sample");
Object obj=objClass.newInstance();
Sample sample=(Sample)obj;//抛出NoClassDefFoundError错误
System.out.println(sample.v1);
}
如果此时Sample.class和Dog.class仅仅复制到D:\myapp\serverlib目录下,运行以上程序会报错,抛出NoClassDefFoundError错误。因为MyClassLoader是由系统类加载器加载,而Sample类是由loader1加载,因此在MyClassLoader类中是看不到Sample类的,因此,当使用Sample类,声明Sample类型变量时,抛出异常。但是如果此时Sample.class和Dog.class复制到D:\myapp\sysrlib目录下,上述程序可以正常运行,因为MyClassLoader类和Sample类都是用系统类加载器加载的,互相可见。
当两个不同命名空间的类想要相互访问时,可以采用java反射机制来访问对方的实例的属性和方法。比如,将main()函数替换为下列内容。程序即可正常运行。
public static void main(String[] args)throws Exception
{
MyClassLoader loader1=new MyClassLoader("loader1");//父加载器为系统类加载器
loader1.setPath("D:\\myapp\\serverlib\\");
Class objClass=loader1.loadClass("Sample");
Object obj=objClass.newInstance();
Field f=objClass.getField("v1");
int v1=f.getInt(obj);
System.out.println(v1);
}
4. 类的卸载
当代表这个类的Class对象不再被引用,即不可触及时,那么Class对象就会结束生命周期,该类在方法区内的数据也会被卸载。由java虚拟机自带的类加载器加载的类,在虚拟机的生命周期中,始终不会被卸载。由用户自定义的类加载器加载的类可以卸载。
通过以下实例讲解类的卸载过程。把Sample类和Dog类放在D:\myapp\serverlib目录下,然后把MyClassLoader类的main()方法替换为如下代码:
public static void main(String[] args)throws Exception
{
MyClassLoader loader1=new MyClassLoader("loader1"); //1
loader1.setPath("D:\\myapp\\serverlib\\"); //2
Class objClass=loader1.loadClass("Sample"); //3
System.out.println("objClass's hashCode is "+objClass.hashCode()); //4
Object obj=objClass.newInstance(); //5
loader1=null; //6
objClass=null; //7
obj=null; //8
loader1=new MyClassLoader("loader1"); //9
objClass=loader1.loadClass("Sample"); //10
System.out.println("objClass's hashCode is "+objClass.hashCode());
}
当程序执行到第5步时,引用变量与对象之间的引用关系如图。
其中,loader1引用变量和obj引用变量间接引用代表Sample类的Class对象,而objClass变量则直接引用它。当程序执行完第8步,所有引用变量都设置为null,此时,Sample对象、MyClassLoader对象都结束生命周期,代表Sample类的Class对象也结束生命周期,方法区的Sample类的二进制数据结构也被卸载。程序执行到第10步时,Sample类被重新加载,堆区会重新生成一个新的代表Sample类的Class实例。所以main()函数运行过程中两次打印的hashCode值不一样。
5. Class对象
在类加载器的内部实现中,是用一个java集合来存放所加载类的引用,因此加载类的引用有成员变量hashCode。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。因此上图中loader1引用的MyClassLoader对象和代表Sample类的Class对象是双向关联关系。
一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有java类还有一个class静态属性,引用代表这个类的Class对象。
Class c1=Sample.class;
Class c2=new Sample().getClass();
Class c3=Class.forName("Sample");
System.out.println(c1==c2);//打印true
System.out.println(c1==c3);//打印true