Day8---类的生命周期(2)

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值