深切怀念传智播客张孝祥老师,特将其代表作——Java并发库视频研读两遍,受益颇丰,记以后阅
自己编写的类只会在用到的时候才载入,称为依需求载入;基础类库是一次性载入的,称为预先载入,这是因为基础类库里头大多是Java程序执行时必备的类,所以为了不要老是做浪费时间的I/O动作(读取文档系统,然后载入记忆体),预先载入这些类库会让Java应用程序在执行时速度稍快一些。依需求载入时,仅仅声明一个类型是不会被加载的,只有实例化才会加载,其有点是节省记忆体,缺点是当第一次用到该类时,系统需要花一些时间来加载该类。(注:如果一个类有父类,载入它之前会先载入父类,类加载器会依继承体系最上层的类往下依序载入)
Java提供两种方法来达成动态行,一种是隐式的,另一种是显式的。这两种方式底层用到的机制完全相同,差异只有程序代码不同。隐式的就是当用到new这个Java关键字时,会让类加载器依需求载入所需的类。显式的又分为两种方法:一种是借用java.lang.Class里的forName()方法,另一种则是借用java.lang.ClassLoader里的loadClass()方法。
一切都是由Bootstrap Loader开始:类加载器的阶层体系
Java程序在编译之后会产生许多的执行单位(.class),当我们执行主类时(public static void main(String arg[])方法的类),才由虚拟机一一载入所有需要的执行单位,变成一个逻辑上为一体的Java应用程序。下面将细部讨论这整个流程。
当我们在命令行输入java xxx.class时,java.exe根据我们之前所提过的逻辑找到JRE,接着找到在JRE之中的jvm.dll(真正的Java虚拟机),最后载入这个动态连结函数库,启动Java虚拟机。
虚拟机一启动,会先做一些初始化的动作,比方说抓取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器,即所谓的Bootstrap Loader,Bootstrap Loader是由C++编写的。这个Loader所做的初始化工作中,除了也做一些基本的初始化动作之外,最重要的就是载入定义在sun.msic命名空间底下的Launcher.java之中的ExtClassLoader(因此是inner class,所以编译之后会变成Launcher$ExtClassLoader.class),并设定其parent为null,代表其父类加载器为Bootstrap Loader。然后Bootstrap Loader再要求载入定义在sun.misc命名空间下的Launcher.java之中的AppClassLoader(因此是inner class,所以编译之后会变成Launcher$AppClassLoader.class),并设定其parent为之前产生的ExtClassLoader实例。这里需要注意的是:Launcher$ExtClassLoader.class与Launcher$AppClassLoader.class都是由Bootstrap Loader所载入,所以parent和由那个类加载器载入没有关系。可以用下图表示:
AppClassLoader在sun官方文件中常常又被称作系统加载器(System Loader)。最后一个步骤,是由AppClassLoader负责载入我们在命令行之中所输入的xxx.class(注意:实际上xxx.class很可能是由ExtClassLoader或Bootstrap Loader载入,参考下一篇博文的【委派模型】),然后开始一个Java应用程序的生命周期。整个流程如下图:
这个由Bootstrap Loader-->ExtClassLoader-->AppClassLoader,就是我们所谓的类加载器的阶层体系。
再次强调,类加载器由谁载入(这句话有点诡异,类加载器也要由类加载器载入,这是因为除了Bootstrap Loader之外,其余的类加载器都是由Java所编写),和它的parent是谁没有关系,parent的存在只是为了某些特殊目的,这个目的之后再作解析。三个主要的加载器的关系如下图:
在此要注意的是,AppClassLoader和ExtClassLoader都是URLClassLoader的子类。由于它们都是URLClassLoader的子类,所以它们也应该有URL作为搜索类的参考,由源代码我们可以得知:
AppClassLoader所参考的URL是从系统参数java.class.path取出的字符串所决定,而java.class.path则是由我们在执行java.exe时,利用-cp或-classpath或CLASSPATH环境变量所决定。在预设定情况下,AppClassLoader的搜索路径为"."(目前所在目录),如果使用-classpath(与-cp等效),就可以改变AppClassLoader的搜索路径,如果没有指定-classpath,就会搜索环境变量CLASSPATH。
ExtClassLoader搜索路径参考系统参数java.ext.dirs,会指向java.exe所选择的JRE所在位置下的\lib\ext子目录。
Bootstrap Loader的搜索路径由系统参数sun.boot.class.path指定。
注意:AppClassLoader和Bootstrap Loader只会搜索指定的路径,不会迂回搜索这些位置下的其他路径或者没有指定的JAR文件。而ExtClassLoader会搜索底下所有的JAR文件以及classes目录,作为其搜索路径。
AppClassLoader和ExtClassLoader在整个虚拟机中只存在一份,一旦建立了,其内部所参考的搜索路径将不再改变,也就是说,即使我们在程序里利用System.setProperty()来改变系统参数的内容,仍然无法更改搜索路径。因此,执行时期动态更改搜索路径的设定是不可能的事情。如果因为特殊需求,有些类的所在路径并非在一开始时就能决定,那么除了产生新的类加载器来辅助我们载入所需要的类之外,没有其它方法了。
委派模型
上一篇提过Bootstrap Loader所做的初始工作中,除了做一些基本的初始化动作之外,最重要的就是载入定义在sun.misc命名空间底下的Launcher.java之中的ExtClassLoader,并设定其parent为null,然后Bootstrap Loader再载入定义在sun.misc命名空间底下的Launcher.java之中的AppClassLoader,并设定其parent为之前产生的ExtClassLoader实例。这就产生了所谓的类加载器阶层体系,如下图:
而之所以有阶层体系的存在,是为了实现委派模型。所谓的委派模型,用简单的话来说,就是类加载器有载入类的需求时,会先请示其parent使用其搜索路径帮忙载入,如果parent找不到,那么才由自己依照自己的搜索路径搜索类。
类加载器的功能
除了达到动态性之外,其实最重要的原因莫过于安全。以下图来说明:
一,假设我们利用RULClassLoader到网络上的任何地方下载了其它的类,URLClassLoader都不可能下载AppClassLoader、ExtClassLoader或者Bootstrap Loader可以找到的同名类(全名),因此,蓄意破坏者根本没有机会植入有问题的程序代码。
二,类加载器无法看到其它相同阶层类加载器所载入的类,如上图所示,图中虚线框起来的部分意指从www.sun.com下载程序代码的类加载器所能看到的类。告诉我们从www.sun.com载入的类,无法看到www.xxx.com载入的类,这除了意味着不同的类加载器可以载入完全相同的类之外,也排除了误用或恶意使用别人程序代码的机会。
加载类的方式有以下几种:
1)从本地系统直接加载
2)通过网络下载.class文件
3)从zip,jar等归档文件中加载.class文件
4)从专有数据库中提取.class文件
5)将Java源文件动态编译为.class文件(服务器)
类加载器的代理模式
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下Java 虚拟机是如何判定两个Java 类是相同的。Java虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个Java 类com.example.Sample
,编译之后生成了字节代码文件Sample.class
。两个不同的类加载器ClassLoaderA
和ClassLoaderB
分别读取了这个Sample.class
文件,并定义出两个java.lang.Class
类的实例来表示这个类。这两个实例是不相同的。对于Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常ClassCastException
。下面通过示例来具体说明。代码清单3中给出了Java 类com.example.Sample
。
清单 3. com.example.Sample 类
package com.example;
public class Sample {
private Sample instance;
public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
如 代码清单3所示,com.example.Sample
类的方法setSample
接受一个java.lang.Object
类型的参数,并且会把该参数强制转换成com.example.Sample
类型。测试 Java 类是否相同的代码如 代码清单4所示。
清单 4. 测试 Java 类是否相同
public void testClassIdentity() {
String classDataRootPath = "C:\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
代码清单4中使用了类FileSystemClassLoader
的两个不同实例来分别加载类com.example.Sample
,得到了两个不同的java.lang.Class
的实例,接着通过newInstance()
方法分别生成了两个类的对象obj1
和obj2
,最后通过 Java 的反射 API 在对象obj1
上调用方法setSample
,试图把对象obj2
赋值给obj1
内部的instance
对象。代码清单4的运行结果如 代码清单5所示。
清单 5. 测试 Java 类是否相同的运行结果
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more
从 代码清单5给出的运行结果可以看到,运行时抛出了java.lang.ClassCastException
异常。虽然两个对象obj1
和obj2
的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被Java 虚拟机认为是相同的。
了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有Java 应用都至少需要引用java.lang.Object
类,也就是说在运行的时候,java.lang.Object
这个类需要被加载到Java 虚拟机中。如果这个加载过程由Java 应用自己的类加载器来完成的话,很可能就存在多个版本的java.lang.Object
类,而且这些类之间是不兼容的。通过代理模式,对于Java 核心库的类的加载工作由引导类加载器来统一完成,保证了Java 应用所使用的都是同一个版本的Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在Java 虚拟机内部创建了一个个相互隔离的Java 类空间。这种技术在许多框架中都被用到,后面会详细介绍。
下面具体介绍类加载器加载类的详细过程。
加载类的过程
在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass
来实现的;而启动类的加载过程是通过调用loadClass
来实现的。前者称为一个类的定义加载器(definingloader),后者称为初始加载器(initiatingloader)。在Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类com.example.Outer
引用了类com.example.Inner
,则由类com.example.Outer
的定义加载器负责启动类com.example.Inner
的加载过程。
方法loadClass()
抛出的是java.lang.ClassNotFoundException
异常;方法defineClass()
抛出的是java.lang.NoClassDefFoundError
异常。
类加载器在成功加载某个类之后,会把得到的java.lang.Class
类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即loadClass
方法不会被重复调用。
下面讨论另外一种类加载器:线程上下文类加载器。
线程上下文类加载器
线程上下文类加载器(context class loader)是从 JDK1.2 开始引入的。类java.lang.Thread
中的方法getContextClassLoader()
和setContextClassLoader(ClassLoadercl)
用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoadercl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java提供了很多服务提供者接口(ServiceProvider Interface,SPI),允许第三方为这些接口提供实现。常见的SPI 有JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如JAXP 的SPI 接口定义包含在javax.xml.parsers
包中。这些 SPI 的实现代码很可能是作为Java 应用所依赖的jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如JAXP 中的javax.xml.parsers.DocumentBuilderFactory
类中的newInstance()
方法用来生成一个新的DocumentBuilderFactory
的实例。这里的实例的真正的类是继承自javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的实现所提供的。如在Apache Xerces 中,实现的类是org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI实现的Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到SPI 的实现类的,因为它只加载Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到SPI 实现的类。线程上下文类加载器在很多SPI 的实现中都会用到。
下面介绍另外一种加载类的方法:Class.forName
。
Class.forName
Class.forName
是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(Stringname, boolean initialize, ClassLoader loader)
和Class.forName(StringclassName)
。第一种形式的参数name
表示的是类的全名;initialize
表示是否初始化类;loader
表示加载时使用的类加载器。第二种形式则相当于设置了参数initialize
的值为true
,loader
的值为当前类的类加载器。Class.forName
的一个很常见的用法是在加载数据库驱动的Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用来加载 ApacheDerby 数据库的驱动。
假设你写了一个java.lang.System类放在你的classPath下面,通常情况是加载不进来的,因为委托机制保证了按照BootStrap-ExtClassLoader-AppClassLoader的顺序加载类,你写的这个类类名与tr.jar的java.lang.System冲突。但是我们可以越过委托机制,使用自己的加载类来加载【不挂在加载树上】。
编写自己的类加载器必须继承一个类ClassLoader,如果你要把你的类加载器挂在树上,那么你只复写findClass。首先调用loadClass去找父亲。。。最后调用findClass。如果不想挂,那么你就复写loadClass。这其实是模板方法设计模式:父类定义机制,子类只需要修改局部。越到上层,越抽象,越就是定义机制和概念,下面的也就是做点儿实际工作。所以一流好公司做标准,二流搞质量。
loadClass/findClass获取二进制数据---defineClass搞成类的字节码
importjava.io.*;
importjava.lang.reflect.*;
publicclass MyClassLoader extends ClassLoader
{
private String path = null;
public MyClassLoader(String path) throwsException//检查文件是否存在
{
File f = new File(path);
if(!f.isDirectory())
{
throw new RuntimeException(path+ " is not a directory");
}
this.path = path;
}
public Class findClass(String name)//throws Exception //为什么不能抛出
{
try
{
File f = newFile(path,name.substring(name.lastIndexOf('.')+1) + ".class");
FileInputStream fis = newFileInputStream(f);
ByteArrayOutputStream bos = newByteArrayOutputStream();
cypher(fis,bos);//解密
byte [] buf =bos.toByteArray();
fis.close();
bos.close();
returndefineClass(name,buf,0,buf.length);
}catch(Exception e)
{
throw newClassNotFoundException(name + " is not found!");
}
return null;
}
public static void cypher(InputStreamistream,OutputStream ostream) throws Exception
{
//下面这段代码可能遇到255的字节,当成byte就成了-1
int b = 0;
while((b = istream.read()) != -1)
{
ostream.write(((byte)b) ^0xff);
}
}
public static void main(String [] args)throws Exception
{
//下面省略了错误检查
if(!args[0].endsWith("class"))
{
ClassLoader loader = newMyClassLoader(args[1]);
Class cls =loader.loadClass(args[0]);
/*
让自定义类继承Date类
System.out.println(cls.getClassLoader().getClass().getName());
java.util.Date d =(java.util.Date)cls.newInstance();
System.out.println(d.toString());
*/
//Method m =cls.getMethod("test",null);//在jdk1.5中报警告,为什么?
Method m =cls.getMethod("test");
//m.invoke(cls.newInstance(),null);
m.invoke(cls.newInstance());
//((Test)cls.newInstance()).test();
return;
}
else
{
FileInputStream fis = newFileInputStream(args[0]);
File f = new File(args[1], newFile(args[0]).getName());//不用检查目录最后是否有目录分割符
FileOutputStream fos = newFileOutputStream(f);
cypher(fis,fos);
fis.close();
fos.close();
}
}
}
//类加载器不能加载这种非public的类
/*
Exceptionin thread "main" java.lang.IllegalAccessException: ClassMyClassLoader
can not access a member of class MyTest withmodifiers ""
*/
/*
classMyTest
{
public void test()
{
System.out.println("hello,www.it315.org");
}
}
*/
l 编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为WebAppClassloader。。。。
l 把MyServlet.class文件打jar包,放到ext目录中,重启tomcat,发现找不到HttpServlet的错误。这是由于MyServlet放到ext目录中之后,首先WebAppClassLoader委托ExtClassLoader去ext找,找到了,发现这个类继承于HttpServlet,就去加载HttpServlet,由于父级类加载器加载的类无法引用只能被子级类加载器加载的类,所以ExtClassLoader在ext目录中找不到HttpServlet,于是报错,把servlet.jar也放到ext目录中,问题解决了,打印的结果是ExtclassLoader 。
l
1, 正常写的MyServlet放在ClassPath下由WebAppClassLoader加载
2, 将MyServlet打包放入ext下,就由ExtClassLoader加载