Java 类加载器
什么是Java 类加载器?
- 类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。
一般来说,Java 虚拟机使用 Java 类的方式如下:
Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。
类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。
每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
类加载的过程
- 加载
类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象。
- 验证
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备
为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了。
需要注意的是这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
- 解析
主要将常量池中的符号引用替换为直接引用的过程。
符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
有类或接口的解析,字段解析,类方法解析,接口方法解析等。
- 初始化
类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
java.lang.ClassLoader类介绍
基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。下面详细介绍这个 Java 类。
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。
除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如下表所示。
方法 | 说明 |
getParent() | 返回该类加载器的父类加载器。 |
loadClass(String name) | 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。 |
findClass(String name) | 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。 |
findLoadedClass(String name) | 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
类加载器的主要结构
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
- 引导类加载器(Bootstrap Class Loader)
Bootstrp加载器是用C++语言写的,负责加载%JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
- 扩展类加载器(Extensions Class Loader)
它用来加载 Java 的扩展库。主要加载%JAVA_HOME%/jre/lib/ext此路径下的所有目录以及java.ext.dirs系统变量指定的路径中类库。
- 应用程序类加载器(AppClassLoader)
面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类.
除了系统提供的类加载器以外,我们可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器。
在引导类加载器之外,所有的类加载器都有一个父类加载器。通过getParent()方法可以得到。
对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。
双亲委派模型
简介
- 类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的;除了启动类加载器,每个类都有其父类加载器(父子关系由组合(不是继承)来实现);
- 所谓双亲委派是指每次收到类加载请求时,先将请求委派给父类加载器完成(所有加载请求最终会委派到顶层的Bootstrap ClassLoader加载器中),如果父类加载器无法完成这个加载(该加载器的搜索范围中没有找到对应的类),子类尝试自己加载。
类加载器之间的协调
每个类加载都有一个父类加载器,我们通过下面的程序来验证。
Public class Test{
Public static void main(String[] arg){
ClassLoader c = Test.class.getClassLoader(); //获取Test类的类加载器
System.out.println(c);
ClassLoader c1 = c.getParent(); //获取c这个类加载器的父类加载器
System.out.println(c1);
ClassLoader c2 = c1.getParent();//获取c1这个类加载器的父类加载器
System.out.println(c2);
}
}
输出结果:
……AppClassLoader……
……ExtClassLoader……
Null
可以看出Test是由AppClassLoader加载器加载的,AppClassLoader的Parent 加载器是 ExtClassLoader。
但是ExtClassLoader的Parent为 null 是怎么回事呢,前面有提到Bootstrap Loader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,所以在java程序代码里试图打印出其内容时,我们就会看到输出为null。
双亲委派模型的优点
- 避免同一个类被多次加载;
- 每个加载器只能加载自己范围内的类,保证了 Java 的核心 API 不被篡改。
自定义类加载器
要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流。
如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。
例子:
package classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
class TestClassLoad {
@Override
public String toString() {
return "类加载成功。";
}
}
public class PathClassLoader extends ClassLoader {
private String classPath;
public PathClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getData(String className) {
String path = classPath + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while ((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String args[]) throws ClassNotFoundException,
InstantiationException, IllegalAccessException {
ClassLoader pcl = new PathClassLoader("D:\\ProgramFiles\\eclipseNew\\workspace\\cp-lib\\bin");
Class c = pcl.loadClass("classloader.TestClassLoad");//注意要包括包名
System.out.println(c.newInstance());//打印类加载成功.
}
}