• 类加载器笔记


    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使用情况,如下:

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

  • 相关阅读:
    H5测试
    【多线程】不懂什么是 Java 中的锁?看看这篇你就明白了!
    【spring】69道Spring面试题和答案
    【数据库】数据库面试知识点汇总
    【小技巧】老程序员总结的 40 个小技巧,长脸了~
    【springboot】集成swagger
    【springboot】集成Druid 作为数据库连接池
    【springboot】整合 MyBatis
    【权限管理】Spring Security 执行流程
    【权限管理】springboot集成security
  • 原文地址:https://www.cnblogs.com/enjoy-ourselves/p/3706857.html
Copyright © 2020-2023  润新知