类加载器笔记

1.双亲委派机制

一个类加载器收到加载某个类的请求以后,先将请求委托给它的父加载器处理,逐级上溯,直到顶级加载器(启动类加载器)。

URLClassLoader(网络类加载器)----->sun.misc.Launcher$AppClassLoader(系统类加载器)----->sun.misc.Launcher$ExtClassLoader(扩展类加载器)----->BootstrapLoader启动类加载器

根据测试,就算将自己写的类或者jar包放在..jre/lib目录(即启动类加载器的默认加载路径)下,然后new URLClassLoader(..,null)指定父级加载器是启动类加载器,启动类加载器也不会帮我们去加载放进去的类。

可以这样认为:启动类加载器只会加载固定的核心jar包中的类,严格限制了自己的加载边界。所以我们指定父级加载器为启动类加载器时,实际上也就是使用我们自己的类加载器,避免了默认时还去classpath,ext包等地方去寻找(默认还会去使用系统和扩展类加载器尝试加载),提高加载效率。

注意:父级加载器并不是父类。从类的层次关系来看,系统类加载器和扩展类加载器都是URLClassLoader的子类

 1 import java.io.File;
 2 import java.lang.reflect.Method;
 3 import java.net.URL;
 4 import java.net.URLClassLoader;
 5 
 6 public class ClassLoader201405041100 {
 7     public static void main(String[] args) throws Exception {
 8         File file = new File("C:\zevun2\Servers\testtest.jar");
 9         URL url = file.toURI().toURL();
10         URLClassLoader urlcl = new URLClassLoader(new URL[]{url});
11         System.out.println("网络类加载器urlcl的父级加载器--"+urlcl.getParent());
12         
13         /* 系统类加载器作为网络类加载器的父级加载器,在classpath底下寻找匹配全名的类文件    
14          * 结果找到了,所以它会直接加载,并把引用返给URLClassLoader
15          * 所以上边那个testtest.jar中的类被忽略
16          * 这种层次结构体现了虚拟机的一种信任机制,优先相信更近的类
17          */
18         Class clazz = urlcl.loadClass("gofAndJavaSourceStudy.studyclassloader.TestLoad3");
19         System.out.println(clazz.getClassLoader());    //得到的是系统类加载器
20         Method m = clazz.getMethod("helloload3", null);
21         String result = (String) m.invoke(clazz.newInstance(), null);
22         System.out.println(result);
23         
24         URLClassLoader urlcl2 = new URLClassLoader(new URL[]{url});
25         System.out.println("urlcl2的父级加载器--"+urlcl2.getParent());
26         Class clazz2 = urlcl2.loadClass("gofAndJavaSourceStudy.studyclassloader.TestLoad3");
27         System.out.println(clazz2.getClassLoader());    //得到的是系统类加载器
28         Method m2 = clazz.getMethod("helloload3", null);
29         String result2 = (String) m2.invoke(clazz2.newInstance(), null);
30         System.out.println(result2);
31         
32         /*
33          * 所以此时这里返回true,因为同属于同一个命名空间
34          */
35         System.out.println(clazz==clazz2);        //false
36         System.out.println(clazz2.equals(clazz));    //false
37         System.out.println(ClassLoader.getSystemClassLoader());
38         
39         //结论:在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,注意是类加载器的实例
40         //URL
41     }
42 }

控制台结果:(此时的结果是在classpath下删掉gofAndJavaSourceStudy.studyclassloader.TestLoad3类)

此时,URLClassLoader两个实例分别在外部加载同一个类,但生成两个TestLoad3 Class对象(对JVM来说两者不同),它们分别属于不同的命名空间

网络类加载器urlcl的父级加载器--sun.misc.Launcher$AppClassLoader@ef137d
java.net.URLClassLoader@b8f8eb
hello load3
urlcl2的父级加载器--sun.misc.Launcher$AppClassLoader@ef137d
java.net.URLClassLoader@56f631
hello load3
false
false
sun.misc.Launcher$AppClassLoader@ef137d

不删除时的结果:

此时,两个URLClassLoader实例委托父级加载器AppClassLoader加载成功,而且这个系统类加载器是同一个对象,所以对JVM来说,同属一个命名空间,只需存在一份TestLoad3的Class实例就行。

网络类加载器urlcl的父级加载器--sun.misc.Launcher$AppClassLoader@ef137d
sun.misc.Launcher$AppClassLoader@ef137d
hello load3
urlcl2的父级加载器--sun.misc.Launcher$AppClassLoader@ef137d
sun.misc.Launcher$AppClassLoader@ef137d
hello load3
true
true
sun.misc.Launcher$AppClassLoader@ef137d

相关阅读:http://blog.csdn.net/lovesomnus/article/details/22860985

2.关于类加载与类初始化

Class.forName("..")默认使用的类加载器是当前调用类的类加载器,等价于 Class.forName(className, true, currentLoader),即会生成该类的类对象Class,并且初始化(即初始化类中的静态变量和静态块)

所以我们使用JDBC驱动程序时,我们自己不必调用DriverManager的注册驱动方法,而是直接用Class.forName("驱动名"),这是因为在每个驱动的实现类中,它在静态块里都会调用DriverManager.registerDriver(Driver driver)注册一个自身的实例

接下来我们就可以直接使用DriverManager.getConnection(..)来获得连接(这也是最初学JDBC疑惑的地方,想为什么Class.forName(..)以后,DriverManager就知道我们注册的驱动了),如以下的演示:

 1 package gofAndJavaSourceStudy.studyclassloader;
 2 
 3 public class TestClassForName {
 4     public static int a = 1;
 5     
 6     static{
 7         System.out.println("类初始化--块初始化开始");
 8         a = 2;
 9         TestClassForName tcfn = new TestClassForName();
10         a = 3;
11         System.out.println("类初始化--块初始化完毕");
12     }
13     public TestClassForName(){
14         System.out.println("实例化"+a);
15     }
16     public void getA(){
17         System.out.println("调用方法getA--"+a);
18     }
19 }

上边这个例子比较有趣,当我在另一个类中加载并实例化它时:

1 Class clazz = Class.forName("gofAndJavaSourceStudy.studyclassloader.TestClassForName");
2 ((TestClassForName)clazz.newInstance()).getA();

分析一下执行过程:

forName方法会加载并初始化类,所以要去执行静态块的代码,执行中碰到要new本类对象,此时虽然类的初始化未完成,但是肯定不会再一次进行类的初始化,我们都知道类只会初始化一次,所以我们可以这样理解,一旦开始类的初始化工作,不管有没有完成,都标记为这个类“已经初始化”,所以此时new对象时就会去打印当前的a值,初始化到什么程度就取多少,所以此时打印出“实例化2”。接下来的就很简单了,初始化完毕后a最终的值为3,所以newInstance()时打印“实例化3”,调用getA打印的也是3.

1 类初始化--块初始化开始
2 实例化2
3 类初始化--块初始化完毕
4 实例化3
5 调用方法getA--3

3.每个类在加载完成后会生成一个该类的Class实例,此时会被自动加上一个常量属性public static final Class class,即我们平常使用的A.class对象.

3.1共有三种获得类实例的方法:

(1)Class.forName("类全名")  等价于 Class.forName(className, true, currentLoader),此时会导致类初始化。

(2)public final Class getClass()  这要求有该对象的实例才能调用

(3)类名.class  此种方法较前两者简单,如果是第一次调用,它不会触发类的初始化。

当首次调用类的静态方法,构造方法,非常量静态域时才会初始化。(访问编译期常量不会导致初始化,这是因为在编译的时候,常量(static final 修饰的)会存入调用类的常量池【这里说的是main函数所在的类的常量池】,调用的时候本质上没有引用到定义常量的类,而是直接访问了自己的常量池。而访问非编译期常量,即在编译时不能确定值的,或者其它的非常量静态域都会导致类的初始化)

编译时常量不会导致类初始化,而static final 变量(在编译时无法确定其值的变量)会导致类初始化,所以为了使性能不下降,在使用final static 修饰一个变量时,尽量不要包含可变因素在这个变量里。

3.2当初始化一个类的时候,发现其父类还没有初始化,那么先去初始化它的父类。这条原则对于接口不适用,即一个类的初始化不会导致它实现的接口初始化,子接口的初始化也不会导致父接口的初始化。

当访问一个静态变量时,只会初始化这个静态变量真正所属的类,如:父类有一个静态成员static int a,用 子类.a 去访问的话只会初始化父类,不会初始化子类。

3.3类加载包括三个步骤:

(1)装载:根据字节码产生二进制数据流,解析这个二进制数据流为方法区中的内部数据结构,在堆上生成一个表示该类型的Class实例。

(2)连接:检查验证字节码是否正确,为类变量分配内存并设置默认初始值,将符号引用替换为直接引用。

(3)初始化:调用<clinit>方法,即执行静态变量初始化语句和静态块的语句。这两部分实际是编译器收集起来放在字节码的<clinit>方法中的。

 4.根据测试,用类加载器在运行时动态加载的类是会回收的,下面的例子共遍历50000次,每次都用一个新的类加载器加载一个代理类,加载类的个数和PermGen使用情况,如下:

很明显,由于在程序中并没有用集合来保存生成的那些类的实例,所以每过一段时间,类也被回收了。

原文地址:https://www.cnblogs.com/enjoy-ourselves/p/3706857.html