1. 类加载器
类加载器(Class Loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就转换成 Java 字节码(.class 文件)。类加载器负责读取 Java 字节码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过实例的 newInstance() 方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。
类加载器有是 Java 类,它也需要被加载器加载,显然必须有第一个不是 Java 类的类加载器—— BootStrap 加载。
2. 类加载器的分类
引导类加载器(BootStrap Class Loder——BootStrap):它用来加载 Java 核心库,是用 C++ 编写的一段二进制代码。扩展类加载器(Extensions Class Loder——ExtClassLoader):它用来加载 Java 的扩展库。Java 虚拟机会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类系统类加载器(System Class Loder——AppClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类,一般来说 Java 应用的类都是由它来完成加载的。这就是为什么我们要把 .class 文件放在 CLASSPATH 里面的原因。
JVM在运行时会产生三个ClassLoader,Bootstrap ClassLoader、Extension ClassLoader和AppClassLoader.
其中,Bootstrap是用C++编写的,我们在Java中看不到它,是null。它用来加载核心类库,在JVM源代码中这样写道:
static const char classpathFormat[] =
"%/lib/rt.jar:"
"%/lib/i18n.jar:"
"%/lib/sunrsasign.jar:"
"%/lib/jsse.jar:"
"%/lib/jce.jar:"
"%/lib/charsets.jar:"
"%/classes";
知道为什么不需要在classpath中加载这些类了吧?人家在JVM启动的时候就自动加载了,并且在运行过程中根本不能修改Bootstrap加载路径。(所以要想自己写JAVA包类,可将其放到RT.JAR里)
Extension ClassLoader用来加载扩展类,即
D:\Program Files\Java\jdk1.6.0_10\jre\lib\ext\classes的类、
D:\Program Files\Java\jdk1.6.0_10\jre\lib\ext中JAR包。
最后AppClassLoader才是加载Classpath的。
ClassLoader加载类用的是委托模型。即先让Parent类(而不是Super,不是继承关系)寻找,Parent找不到才自己找。看来ClassLoader还是蛮孝顺的。三者的关系为:AppClassLoader的Parent是ExtClassLoader,而ExtClassLoader的Parent为Bootstrap ClassLoader。加载一个类时,首先BootStrap先进行寻找,找不到再由ExtClassLoader寻找,最后才是AppClassLoader。
为什么要设计的这么复杂呢?其中一个重要原因就是
安全性。比如在Applet中,如果编写了一个java.lang.String类并具有破坏性。假如不采用这种委托机制,就会将这个具有破坏性的String加载到了用户机器上,导致破坏用户安全。但采用这种委托机制则不会出现这种情况。因为要加载java.lang.String类时,系统最终会由Bootstrap进行加载,这个具有破坏性的String永远没有机会加载。
我们来看这段代码:
public class A {
public static void main(String[] args) {
A a = new A();
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(a.getClass().getClassLoader());
B b = new B();
b.print();
}
}
public class B {
public void print() {
System.out.println(this.getClass().getClassLoader());
}
}
1、我们将它放在Classpath中,则打印出
sun.misc.Launcher$AppClassLoader@92e78c
sun.misc.Launcher$AppClassLoader@92e78c
可见都是由AppClassLoader来加载的。
2、我们将其放在%jre%/lib/ext/classes(即ExtClassLoader的加载目录。其加载/lib/ext中的jar文件或者子目录classes中的class文件)中。则会打印出:
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$ExtClassLoader
3、我们将A.class放到%jre%/lib/ext/classes中,而将B.class放到classpath中又会怎么样呢?结果是:
sun.misc.Launcher$ExtClassLoader
Exception in
thread "main" java.lang.NoClassDefFoundError:B
at A.main(A.java:6)
怎么会这样呢?这其中有一个重要的问题:A类当然是由ExtClassLoader来加载的,B类要由哪个加载呢?B类要由调用它自己的类的类加载器(真拗口)。也就是说,A调用了B,所以B由A的类加载器ExtClassLoader来加载。ExtClassLoader根据委托机制,先拜托Bootstrap加载,Bootstrap没有找到。然后它再自己寻找B类,还是没找到,所以抛出
异常。ExtClassLoader不会请求AppClassLoader来加载!你可能会想:这算什么问题,我把两个类放到一起不就行了?
呵呵,没这么简单。比如JDBC是核心类库,而各个数据库的JDBC驱动则是扩展类库或在classpath中定义的。所以JDBC由Bootstrap ClassLoader加载,而驱动要由AppClassLoader加载。等等,问题来了,Bootstrap不会请求AppClassLoader加载类啊。那么,
他们怎么实现的呢?我就涉及到一个Context ClassLoader的问题,调用Thread.getContextClassLoader。
3. 除了引导类加载器,其它的加载器如扩展和系统都不能加载JAVA.包
4. 类加载器选择机制
首先当
线程的类加载器去加载线程中的第一个类,然后如果类 A 中引用了类 B ,Java 虚拟机将使用加载类 A 的加载器来加载类 B,还可以直接调用 ClassLoader.loadClass() 方法来指定某个类加载器去加载某个类。也就是说,类加载器在尝试自己去查找某个类的字节码文件时,会先委托给父级加载器先去尝试加载这个类,依次类推。这种模式又称为模版模式。当所有父级加载器并没有加载到该类时,会返回给发起者类加载器,如果仍然没有加载到,则会抛出 ClassNotFoundException 异常,不会再委托给子加载器。
5.
自定义类加载器
自定义加载器类必须继承 ClassLoader 类。在一般情况下只需要重写 ClassLoader 类中的 findClass() 方法
编程实例:编写一个自己的类加载器,对已经加密的 .class 文件只能用我们自己写的类加载器加载。
6. Java 虚拟机是如何判定两个 Java 类是相同的
首先需要说明一下 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 类
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 class FileClassLoader extends ClassLoader {
public static final String drive = "E:\\workspace-nfjd\\classLoader\\bin\\";
public static final String fileType = ".class";
public Class findClass(String name) {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
}
public byte[] loadClassData(String name) {
FileInputStream fis = null;
byte[] data = null;
try {
fis = new FileInputStream(new File(drive + name + fileType));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int ch = 0;
while ((ch = fis.read()) != -1) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return data;
}
}
public class TestClassIdentity {
public static void main(String[] args) throws MalformedURLException {
FileClassLoader fscl1=new FileClassLoader();
FileClassLoader fscl2=new FileClassLoader();
String className = "Sample";
try {
Class<?> class1 = fscl1.findClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.findClass(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 类空间。这种技术在许多框架中都被用到,后面会
详细介绍。
7. Package权限
为了深入了解Java的ClassLoader机制,我们先来做以下实验:
package java.lang;
public class Test {, F" t6 A/ H( D
public static void main(String[] args) {
char[] c = "1234567890".toCharArray();
String s = new String(0, 10, c);
}
}
String类有一个Package权限的
构造函数:* F! X1 K" t( l. |* }
String(int offset, int length, char[] array)
按照默认的
访问权限,由于Test属于java.lang包,因此理论上应该可以访问String的这个构造函数。编译通过!执行时结果如下:
[/td][/tr][tr][td=2,1]Exception in thread "main" java.lang.SecurityException: Prohibited package name:
java.lang% H6 o$ R/ w/ U+ u
at java.lang.ClassLoader.defineClass(Unknown Source
at java.security.SecureClassLoader.defineClass(Unknown Source))
at java.net.URLClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.access$100(Unknown Source)" 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)7 } at java.lang.ClassLoader.loadClassInternal(Unknown Source)奇怪吧?要弄清为什么会有SecurityException,就必须搞清楚ClassLoader的机制。
Java的ClassLoader就是用来动态装载class的,ClassLoader对一个class只会装载一次,JVM使用的ClassLoader一共有4种}
启动类装载器,标准扩展类装载器,类路径装载器和网络类装载器。
这4种ClassLoader的优先级依次从高到低,使用所谓的“双亲委派模型”。 确切地说,如果一个网络类装载器被请求装载一个java.lang.Integer,它会首先把请求发送给上一级的类路径装载器,如果返回已装载,则网络 类装载器将不会装载这个java.lang.Integer,如果上一级的类路径装载器返回未装载,它才会装载java.lang.Integer。
类似的,类路径装载器收到请求后(无论是直接请求装载还是下一级的ClassLoader上传的请求),它也会先把请求发送到上一级的标准扩展类装载器,这 样一层一层上传,于是启动类装载器优先级最高,如果它按照自己的方式找到了java.lang.Integer,则下面的ClassLoader 都不能再装载java.lang.Integer,尽管你自己写了一个java.lang.Integer,试图取代核心库的 java.lang.Integer是不可能的,因为自己写的这个类根本无法被下层的ClassLoader装载。
再说说Package权限。Java语言规定,在同一个包中的class,如果没有修饰符,默认为Package权限,包内的class都可以访问。但是这还不够准确。确切的说,只有由同一个ClassLoader装载的class才具有以上的Package权限。比如启动类装载器装载了java.lang.String,类路径装载器装载了我们自己写的java.lang.Test,它们不能互相访问对方具有Package权限的方法。这样就阻止了恶意代码访问核心类的Package权限方法。