Java中关于类加载器的一些事儿(一)

来源:互联网 发布:xp连接网络打印机步骤 编辑:程序博客网 时间:2024/05/16 15:49

类加载器:

虚拟机设计团队把类加载阶段中的”通过一个类的全限定名来获取描述此类的二进制字节流”的这个动作放到了Java虚拟机的外部去实现,以便让应用程序自己可以决定如何去获取需要的类。这个动作的代码模块称为:类加载器。

类加载可以说是Java语言的一项创新,也是Java语言流行的重要原因之一,它最初是为了满足Applet的需要而开发出来,尽管Applet已经死掉,但是类加载器却在类层次划分,OSGi,热部署,代码加密等领域大房异彩,成为了Java技术体系中的一块重要的基石。


类与类加载器:

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。这句话通俗易懂就是:比较两个类是否相等,只有在这两个类都是由一个类加载器加载的前提下才有意义,否则,即使这两个类来源同一个Class文件,同一个虚拟机加载,只要它们的类加载器不同的话,那么这两个类就必定不会相等。这里所谓的相等,包括代表类的Class对象的equals方法,isAssignableFrom方法和instanceof关键字返回的结果。如果没有注意到类加载的影响的话,很容易产生迷惑的结果,例如:


为什么输出false ?

答:由于自定义的ClassLoader重写了loadClass方法,破坏了JDK的双亲委派机制,导致ClassLoaderTest被JDK的App类加载器和我们自定义的类加载器加载两次,obj实例是使用我们自定义的类加载器加载的,而instanceOf后面的ClassLoaderTest是JDK的App类加载加载的,所以输出为False。


双亲委派模型:

从Java虚拟机的角度来讲,只存在两种类加载器。一种是启动类加载器,这个类加载器由C++语言实现,是JDK的一部分。另一类就是其他的所有的类加载器,这些类加载器都是由Java语言实现,独立于虚拟机外部,并且全都继承至抽象类java.lang.ClassLoader。

 

从Java开发人员角度来看,类加载器可以划分的更细致一些,绝大部门Java程序都会使用到下面三种系统提供的类加载器。

1:启动类加载器,主要加载如下目录中的类:

C:\Program Files\Java\jdk1.8.0_111\jre\lib\resources.jarC:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jarC:\Program Files\Java\jdk1.8.0_111\jre\lib\sunrsasign.jarC:\Program Files\Java\jdk1.8.0_111\jre\lib\jsse.jarC:\Program Files\Java\jdk1.8.0_111\jre\lib\jce.jarC:\Program Files\Java\jdk1.8.0_111\jre\lib\charsets.jarC:\Program Files\Java\jdk1.8.0_111\jre\lib\jfr.jarC:\Program Files\Java\jdk1.8.0_111\jre\classes

或者被Xbootclasspath参数指定目录中的类。如上目录输出可以使用如下代码输出:

System.out.println(System.getProperty("sun.boot.class.path").replace(";", "\n"));

使用VM参数配置启动类加载器加载路径:

-Xbootclasspath/a:C:\Users\Daxin\Desktop\ALL\jar\fuck.jar;// 附加在引导类路径末尾-Xbootclasspath/p:C:\Users\Daxin\Desktop\ALL\jar\fuck.jar;// 置于引导类路径之前

2:拓展类加载器,主要加载的目录如下:


C:\Program Files\Java\jdk1.8.0_111\jre\lib\extC:\WINDOWS\Sun\Java\lib\ext

或者被java.ext.dirs指定的目录。使用如下代码打印输出ext加载路径:

System.out.println(System.getProperty("java.ext.dirs").replace(";", "\n"));

使用VM参数配置加载路径:

-Djava.ext.dirs="C:\Users\Daxin\Desktop\ALL\jar"//注意后面是目录路径,切记。这种方式会覆盖掉JDK之前的ext加载路径,所以配置时候最好把JDK之前的路径一并显示加上,路径之间使用”;”分割。例如:

-Djava.ext.dirs="C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext;C:\Users\Daxin\Desktop\ALL\jar"

3:应用程序类加载器。

由于这个类加载器是ClassLoader.getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器。它负责加载用户路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器的话,一般情况是App类加载器就是程序中的默认加载器。


我们应用程序一般都是由上述三个类加载器相互配合进行加载的,如果有必要的话还可以加入自己定义的类加载器。这些类加载器之间关系图如下:



双亲委派模型除了顶层启动类加载器之外,所有的类加载器都有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的方式实现,而是使用组合(代理关系) 来复用父类加载器代码。

 

拓展:

如果我们自己创建一个java.lang的包,然后在里面创建一个String.java。然后发现可以编译通过,但是永远也无法被加载。

情况1:没有破坏双亲委派前提下

无论是使用系统类加载器还是使用自己定义的类加载器(自定义类加载器重写findClass方法实现加载,这种条件是没有破坏双亲委派)去加载,都无法加载。原因:由于双亲委派原则的存在,每一次加载String.class时都会委派给父类加载器,最终委派给了启动类加载器,启动类加载器发现String已经加载过了,所以不再进行加载直接返回启动类加载器加载的class文件。

 

情况2:破坏了双亲委派前提下

我们可以自定义一个类加载器,然后将类加载的逻辑写到loadClass中,这样就不在委派给父类加载器了,此时强制加载我们自己的java.lang.String.class时候,汇报异常如下:java.lang.SecurityException: Prohibited package name: java.lang


双亲委派实现思路:

接下来看一下ClassLoader的loadClass双亲委派的实现:

 protected Class<?> loadClass(String name, boolean resolve)        throws ClassNotFoundException    {        synchronized (getClassLoadingLock(name)) {            // First, check if the class has already been loaded            Class<?> c = findLoadedClass(name);     //1            if (c == null) {                long t0 = System.nanoTime();                try {                    if (parent != null) {         //2                        c = parent.loadClass(name, false);                    } else {                        c = findBootstrapClassOrNull(name);                    }                } catch (ClassNotFoundException e) {                    // ClassNotFoundException thrown if class not found            //3                     // from the non-null parent class loader                }                if (c == null) {                    // If still not found, then invoke findClass in order                    // to find the class.                    long t1 = System.nanoTime();                    c = findClass(name);                                       //4                    // this is the defining class loader; record the stats                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                    sun.misc.PerfCounter.getFindClasses().increment();                }            }            if (resolve) {                resolveClass(c);            }            return c;        }    }

重点代码1、2、3、4处,接下来分处分析:

1:判断 当前是否加载过该class文件,如果加载过的话直接返回

2:如果c==null的话。委派给双亲加载

3:双亲层层没有加载到的话,直接排除异常,此处异常信息被吞噬掉了

4:调用系统或者自己定义的类加载器加载,如果还加载不到则抛出异常

 

拓展:同一份字节码不会被两个类加载器都加载,因为加载时候会判断该字节码是否加载过。





参考:《深入理解Java虚拟机》第二版

原创粉丝点击