47、类加载器的父亲委托机制

类加载器的父亲委托机制(Parent Delegation)     

        类加载器用来把类加载到java虚拟机中。从JDK1.2开始,类的加载过程采用父亲委托机制。这种机制能更好的保证java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。

1、类加载器

47、类加载器的父亲委托机制_第1张图片

父子加载器并非继承关系,,也就是说子加载器不一定是继承了父加载器。

      除了以上虚拟机自带的加载器以外,用户还可以定制自己的类加载器(User-defined Class Loader)。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类

47、类加载器的父亲委托机制_第2张图片

在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根加载器以外,其他加载器有且只有一个父加载器。

47、类加载器的父亲委托机制_第3张图片

47、类加载器的父亲委托机制_第4张图片

若有一个类加载器能成功加载Sample类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象的引用的类加载器(包括定义类加载器)都被称为初始类加载器。

定义类加载器:如果某个类加载器能够加载一个类,那么该类加载器就称作:定义类加载器;定义类加载器及其所有子加载器都称作:初始类加载器。

当生成一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器就将成为该类加载器的父加载器

2、命名空间

      每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

3、运行时包

      由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看他们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。假设用户自己定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的包可见成员。

4、创建用户自定义的类加载器

47、类加载器的父亲委托机制_第5张图片

[java] view plain copy print ?
  1. import java.io.ByteArrayOutputStream; 
  2. import java.io.File; 
  3. import java.io.FileInputStream; 
  4. import java.io.InputStream; 
  5.  
  6. public class MyClassLoader extends ClassLoader 
  7.     private String name; //类加载器的名字 
  8.     private String path = "d:\\"
  9.     private final String fileType = ".class"
  10.      
  11.     public MyClassLoader(String name) 
  12.     { 
  13.         super(); 
  14.         this.name = name; 
  15.     } 
  16.     public MyClassLoader(ClassLoader parent,String name) 
  17.     { 
  18.         super(parent); 
  19.         this.name = name; 
  20.     } 
  21.     @Override 
  22.     public String toString() 
  23.     { 
  24.         return this.name; 
  25.     } 
  26.     public String getName() 
  27.     { 
  28.         return name; 
  29.     } 
  30.     public void setName(String name) 
  31.     { 
  32.         this.name = name; 
  33.     } 
  34.     public String getPath() 
  35.     { 
  36.         return path; 
  37.     } 
  38.     public void setPath(String path) 
  39.     { 
  40.         this.path = path; 
  41.     } 
  42.     public String getFileType() 
  43.     { 
  44.         return fileType; 
  45.     } 
  46.     protected Class<?> findClass(String name) throws ClassNotFoundException 
  47.     { 
  48.         byte[] data = this.loadClassData(name); 
  49.          
  50.         return this.defineClass(name, data, 0,data.length); 
  51.     } 
  52.      
  53.     private byte[] loadClassData(String name) 
  54.     { 
  55.         InputStream is = null
  56.         byte[] data = null
  57.         ByteArrayOutputStream baos = null
  58.         try 
  59.         { 
  60.             this.name = this.name.replace(".", "\\");            
  61.             is = new FileInputStream(new File(path + name + fileType)); 
  62.             baos = new ByteArrayOutputStream(); 
  63.             int ch = 0
  64.             while(-1 !=(ch = is.read())) 
  65.             { 
  66.                 baos.write(ch); 
  67.             } 
  68.             data = baos.toByteArray(); 
  69.              
  70.         } catch (Exception e) 
  71.         { 
  72.             e.printStackTrace(); 
  73.         }finally 
  74.         { 
  75.             try 
  76.             { 
  77.                 is.close(); 
  78.                 baos.close(); 
  79.             }catch(Exception e) 
  80.             { 
  81.                 e.printStackTrace(); 
  82.             } 
  83.         } 
  84.          
  85.         return data; 
  86.     } 
  87.      
  88.     public static void main(String[] args) throws Exception 
  89.     { 
  90.         MyClassLoader loader1 = new MyClassLoader("loader1"); 
  91.         loader1.setPath("d:\\myapp\\serverlib\\"); 
  92.         MyClassLoader loader2 = new MyClassLoader(loader1,"loader2"); 
  93.         loader2.setPath("d:\\myapp\\clientlib\\"); 
  94.         MyClassLoader loader3 = new MyClassLoader(null,"loader3"); 
  95.         loader3.setPath("d:\\myapp\\otherlib\\"); 
  96.         test(loader2); 
  97.         test(loader3); 
  98.     } 
  99.     public static void test(ClassLoader loader) throws Exception 
  100.     { 
  101.         Class clazz = loader.loadClass("Sample"); 
  102.         Object object = clazz.newInstance(); 
  103.     } 
  104.  
  105. public class Sample 
  106.     public int v1 = 1
  107.     public Sample() 
  108.     { 
  109.         System.out.println("Sample is load by :" + this.getClass().getClassLoader()); 
  110.          
  111.         new Dog(); 
  112.     } 
  113.  
  114. public class Dog 
  115.     public Dog() 
  116.     { 
  117.         System.out.println("dog is load by :"+ this.getClass().getClassLoader()); 
  118.     } 


实验环境:在d盘建有一目录myapp,在其下有四个子目录:syslib、serverlib、clientlib、otherlib,将MyClassLoader.class拷贝到syslib下,将Dog.class和Sample.class两个文件同时拷贝到serverlib和otherlib目录下。

在命令窗口,进入syslib目录,执行java MyClassLoader       输出结果为:

D:\myapp\syslib>java MyClassLoader
Sample is load by :loader1
dog is load by :loader1
Sample is load by :loader3
dog is load by :loader3

分析:

在MyClassLoader中定义了三个变量:loader1、loader2、loader3,代表三个类加载器,他们的父子关系如上图。

loader1的父加载器是loader2,loader2的父加载器是系统加载器,loader3的父加载器是根加载器。

执行test(loader2)时,就是使用加载器loader2来加载Sample类,loader2先查看有无父加载器,有则使用父加载器加载,其父加载器执行相同的操作,找父加载器,在使用父加载器加载,直到没有父加载器时,就用自身加载,如果无法加载,用其子类加载,这里就是loader2调用其父类加载器loader1,loader1调用系统加载器,一直到根加载器,根加载器没有父类,就尝试自己加载,因为在其目录中(sun.boot.class.path所定义目录)没有所请求的类(Sample),就返回给其子类加载,即扩展类加载器,也没有(其目录java.ext.dirs 或jre\lib\ext目录),使用系统类加载器(其目录为环境变量classpath定义或系统属性java.class.path)这里classpath为“.”即当前目录,所以也加载失败,然后调用loader1进行加载,loader1的目录从程序中可以看到为d:\myapp\serverlib\,这里面正好有Sample类,所以加载成功,定义类加载器就为loader1,加载成功后,生成Sample对象,调用了new Dog(),属于对类的主动使用,也进行类的加载,默认使用调用者的定义类加载器即loader1,然后如上述步骤执行加载(即也执行父亲委托机制加载),最后由loader1加载成功。

test(loader3)先用根加载器加载,不成功由loader3加载,loader3的目录为d:\myapp\otherlib,其中有Sample和Dog,加载成功。

如果将serverlib中的Dog.class移至syslib目录,则结果:

D:\myapp\syslib>java MyClassLoader
Sample is load by :loader1
dog is load by :sun.misc.Launcher$AppClassLoader@42e816
Sample is load by :loader3
dog is load by :loader3

因为加载Dog时,syslib为当前目录,即“.”,是系统类加载器的路径,所以系统类加载器加载加载Dog成功。

恢复原来的文件位置,使用java -cp .;d:\myapp\serverlib MyClassLoader ,结果如下

D:\myapp\syslib>java -cp .;d:\myapp\serverlib MyClassLoader
Sample is load by :sun.misc.Launcher$AppClassLoader@addbf1
dog is load by :sun.misc.Launcher$AppClassLoader@addbf1
Sample is load by :loader3
dog is load by :loader3

java的cp选项就是设定classpath路径,这里设置为“.;d:\myapp\serverlib“,即系统类加载器的路径,所以对于loader2来说,系统类加载器先加载Sample和Dog成功

5、加载完成后的内存映像关系

47、类加载器的父亲委托机制_第6张图片

同一个类被加载了两次,loader1加载一次,loader3加载一次。因为是不同的加载器,且不是父子关系,是不同的命名空间,相互不能访问。

6、不同类加载器的命名空间关系

同一个命名空间内的类是互相可见的;子加载器的命名空间包含所有父加载器的命名空间,因此由子加载器加载的类能看见父加载器加载的类,例如系统类加载器加载的类能看见根类加载器加载的类;由父加载器加载的类不能看见子加载器加载的类;如果两个加载器之间没有直接或间接的父子关系,那么他们各自加载的类相互不可见。

对MyClassLoader进行部分修改:

[java] view plain copy print ?
  1. public static void main(String[] args) throws Exception 
  2.     { 
  3.         MyClassLoader loader1 = new MyClassLoader("loader1"); 
  4.         loader1.setPath("d:\\myapp\\serverlib\\"); 
  5.         MyClassLoader loader2 = new MyClassLoader(loader1,"loader2"); 
  6.         loader2.setPath("d:\\myapp\\clientlib\\"); 
  7. //      MyClassLoader loader3 = new MyClassLoader(null,"loader3"); 
  8. //      loader3.setPath("d:\\myapp\\otherlib\\"); 
  9. //      test(loader2); 
  10. //      test(loader3); 
  11.          
  12.         Class clazz = loader1.loadClass("Sample"); 
  13.         Object object = clazz.newInstance(); //创建一个Sample类对象 
  14.          
  15.         Sample sample = (Sample)object; 
  16.         System.out.println(sample.v1); 
  17.     } 


运行结果出异常:

D:\myapp\syslib>java MyClassLoader
Sample is load by :loader1
dog is load by :loader1
Exception in thread "main" java.lang.NoClassDefFoundError: Sample
        at MyClassLoader.main(MyClassLoader.java:103)
Caused by: java.lang.ClassNotFoundException: Sample
        at java.net.URLClassLoader$1.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        ... 1 more
错误103就是  Sample sample = (Sample)object;这一行;

MyClassLoader类由系统类加载器加载,而Sample类由loader1类加载器加载,因此MyClassLoader类看不见Sample类。在MyClassLoader的main方法中使用Sample类,会导致NoClassDefFoundError错误。当两个不同命名空间内的类互相不可见时,可采用java的反射机制来访问对方实例的属性和方法。如下

47、类加载器的父亲委托机制_第7张图片

使用反射来访问sample对象的v1属性:

[java] view plain copy print ?
  1. public static void main(String[] args) throws Exception 
  2.     { 
  3.         MyClassLoader loader1 = new MyClassLoader("loader1"); 
  4.         loader1.setPath("d:\\myapp\\serverlib\\"); 
  5.         MyClassLoader loader2 = new MyClassLoader(loader1,"loader2"); 
  6.         loader2.setPath("d:\\myapp\\clientlib\\"); 
  7. //      MyClassLoader loader3 = new MyClassLoader(null,"loader3"); 
  8. //      loader3.setPath("d:\\myapp\\otherlib\\"); 
  9. //      test(loader2); 
  10. //      test(loader3); 
  11.          
  12. //      Class clazz = loader1.loadClass("Sample"); 
  13. //      Object object = clazz.newInstance(); //创建一个Sample类对象 
  14. //       
  15. //      Sample sample = (Sample)object; 
  16. //      System.out.println(sample.v1); 
  17.          
  18.         Class clazz  = loader1.loadClass("Sample"); 
  19.         Object object = clazz.newInstance(); 
  20.          
  21.         Field field = clazz.getField("v1"); 
  22.          
  23.         int v1 = field.getInt(object); 
  24.         System.out.println(v1); 
  25.     } 

7、类的卸载

当Sample类被加载、连接和初始化后,他的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。由此可见,一个类何时结束生命周期,取决于代表他的Class对象何时结束生命周期。

由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载,java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

修改MyClassLoader的main()方法:

[java] view plain copy print ?
  1. public static void main(String[] args) throws Exception 
  2.     { 
  3.         MyClassLoader loader1 = new MyClassLoader("loader1"); 
  4.         loader1.setPath("d:\\myapp\\serverlib\\"); 
  5.         MyClassLoader loader2 = new MyClassLoader(loader1,"loader2"); 
  6.         loader2.setPath("d:\\myapp\\clientlib\\"); 
  7.         MyClassLoader loader3 = new MyClassLoader(null,"loader3"); 
  8.         loader3.setPath("d:\\myapp\\otherlib\\"); 
  9. //      test(loader2); 
  10. //      test(loader3); 
  11.         Class objClass = loader1.loadClass("Sample"); 
  12.         System.out.println(objClass.hashCode()); 
  13.         Object obj = objClass.newInstance(); 
  14.         loader1 = null
  15.         objClass = null
  16.         obj = null
  17.         loader1 = new MyClassLoader("loader1"); 
  18.         loader1.setPath("d:\\myapp\\serverlib\\"); 
  19.         objClass = loader1.loadClass("Sample"); 
  20.         System.out.println(objClass.hashCode()); 
  21.     } 


打印的结果显示两个不同的hashcode值

运行以上程序,Sample类由loader1加载。在类加载器的内部实现中,用一个java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用他的类加载器,调用Class对象的getClassLoader()方法,就能获得他的类加载器。由此可见,代表Sample类的Class实例与loader1之间为双向关联关系。

      一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,他引用代表这个类的Class对象,例如:

到第一次object生成后,引用变量与对象之间的引用关系如图:

47、类加载器的父亲委托机制_第8张图片

从上图,loader1变量和obj变量间接引用代表Sample类的Class对象,而objClass变量则直接引用他。当所有变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。当程序往下执行时,Sample类又重新被加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例。

程序结果两次打印的hashcode值不同,因此objClass变量两次引用不同的Class对象,可见在Java虚拟机的生命周期中,对Sample类先后加载了两次。

你可能感兴趣的:(java开发工具)