Java语言在刚刚诞生的时候提出过一句著名的口号“一次编写,到处运行”,这句话充分的表达了开发人员对于冲破平台界限的渴望,也解释了Java语言跟平台无关的设定。
一、概述
类加载过程包括加载、连接和初始化,连接又可以细化为验证、准备和解析,除了加载过程可以由程序自定义处理外,其他的过程都是由虚拟机自动处理的;在这个过程中,类是如何被加载到内存中的呢?加载的时候需要加载哪些类呢?这就是本文要讨论的内容了--类加载器。
根据虚拟机规范,类加载器的作用包括根据类的全限定名找到定义此类的二进制字节流;将字节流代表的静态存储类型转化为方法区运行时数据结构;在方法区生成class对象,作为方法区类的入口。虚拟机规范只是规定了整个加载的过程,但是没有规定具体从什么文件中加载字节流,也没有规定如何加载,什么时候加载等等,每个类加载器可以根据实际情况具体实现。
二、类与类加载器
类加载器虽然只是用于类的加载动作,但是类加载器在程序中的作用远不止加载过程。在Java语言中,如果要定义一个类,不仅需要类本身信息,还需要有类加载器的信息,比如我们要判断两个类是否相等,不仅要判断这两个类是否来源于同一个class文件,还要判断是否是同一个类加载器加载的,因为每个类加载器都会有一个独立的命名空间,不同的类加载器加载的类也是不同的。
上文所指的"相等",不仅包括Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象的关系所属判定等,如果没有注意类加载器的影响,那么结果可能会是错误的。举例说明:
package com.jvm.classloader;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object o = classLoader.loadClass("com.jvm.classloader.ClassLoaderTest").newInstance();
System.out.println(o.getClass());
System.out.println(o instanceof com.jvm.classloader.ClassLoaderTest);
}
}
输出结果:
上述代码构造了一个简单的类加载器,它可以加载统一路径下的class文件,我们使用这个类加载器加载类,然后将这个类实例化,打印这个类,可以看到确实是目前所在类,但是判断这个类和class对象关系,却发现是false,这是为什么呢?因为main方法再启动的时候已经使用了类加载器了,这个类加载器是应用类加载器,它已经将class文件加载进虚拟机,自定义类加载器生成的类跟应用类加载器生成的类是不相等的,所以返回false。
三、类加载机制
1、类加载器分类
从Java虚拟机的角度来考虑,类加载器总共有两种,一种是启动类加载器,这个类加载器用C++实现,是虚拟机自身的一部分;另一种是其他类加载器,这些类加载器由Java语言实现,独立于虚拟机,并且全部继承自抽象泪java.lang.ClassLoader。
从开发人员的角度来考虑,Java虚拟机还可以划分的更加详细,Java虚拟机在功能上可以分为:启动类加载器、扩展类加载器、应用类加载器、自定义类加载器。
启动类加载器负责将存放在jre/lib目录中的jar加载到虚拟机中,或者被-Xbootclasspath参数所指定的路径中的jar,并且这些jar是需要被虚拟机识别的(按照文件名识别,例如rt.jar,名字不一致不会被加载)。启动类加载器无法被程序直接引用,用户自定义类加载器如果需要引用启动类加载器,那么直接将父类加载器设置为null即可。
扩展类加载器负责将存放在jre/lib/ext目录下的jar加载到虚拟机中,或者被java.ext.dirs系统变量所指定的目录下的路径中的类库,开发者可以直接使用这个类加载器。
应用类加载器负责将classpath路径下的类库,开发者可以直接使用这个类加载器,这个类加载器也是用户程序默认的类加载器。这个类加载器是ClassLoader的getSystemClassLoader()方法返回的类加载器,所以也被称为系统类加载器。
Java虚拟机提供了这么多的类加载器,我们在程序中改怎么选择类加载器呢?如果一个类被多个类加载器加载如何处理呢?如何保证一个类在程序中只能被一个类加载器加载一次呢?这个问题就是虚拟机的类加载机制--双亲委派模型。
2、双亲委派模型
Java虚拟机的类加载采用父类委托机制,这种类加载器之间的层次关系,称为双亲委派模型。双亲委派模型要求除了顶层的类加载器之外,其他的类加载器都要有父类加载器,这里的类加载器的父子关系不是普通Java语言的继承(Inheritance)关系,而是通过组合(Composition)关系实现的,双亲委派模型如下图所示:
双亲委派模型的工作机制:
1) 如果一个类加载器接到了加载类的请求