在类加载过程的加载阶段,“通过一个类的全限定名来获取描述此类的二进制字节流”是通过“类加载器”来实现的。类加载器虽然只用于实现类的加载动作,但在Java程序中起到的作用不限于类加载阶段。比较连个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。
在JDK1.8中有四种类加载器,如下图所示
注:启动类加载器是用c++编写的,无法查看源码;扩展的类加载器只能加载jar文件包
一、双亲委派模式
所谓双亲委派,就是指用类加载器的loadClass方法时查找类的规则:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中(Bootstrap),只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,使Java类随它的类加载器一起具备了带有优先级的层次关系
。(例如java.lang.Object,无论哪一个类加载器加载这个类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载环境中都是同一个类)
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);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// 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);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
二、线程上下文类加载器
双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办?而且这种情况并不少见,其经常发生在SPI中。SPI是一种服务发现机制,它通过ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。(关于SPI,详见这篇博文)
我们在使用JDBC时,都需要加载Driver驱动,但是如果不写
Class.forName("com.mysql.jdbc.Driver")
也可以让com.mysql.jdbc.Driver正确加载,这是如何做到的呢?我们追踪一下源码
public class DriverManager{
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
static{
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
显然,DriverManager在JAVA_HOME/jre/lib目录下,所以加载器是Bootstrap ClassLoader,但是在该目录下显然没有mysql-connector-5.1.47.jar包,在DriverManager的静态代码块中,怎么能正确加载com.mysql.jdbc.Driver呢,继续看loadInitialDrivers()方法:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
//1)使用ServiceLoader机制加载驱动,即SPI
AccessController.doPrivileged(new PrivilegedAction<void>(){
public Void run() {
ServiceLoader<Driver> loadedDrivers =ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2)使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
先看2) 它最后使用Class.forName完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
再看1)它是大名鼎鼎的Service Provider interface(SPI),约定如下,在jar包的META-INF/services包下,以接口全限定名为文件,文件内容是实现类名称
这样可以使用
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
来得到实现类,体现面向接口编程+解耦的思想。
接着看ServiceLoader.load方法:
public static <S> ServiceLoader<S> load(Class<S> service){
//获取线程上下文类加载器
ClassLoader c1=Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service,c1);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器。换句话说,一个由Bootstrap ClassLoader加载的代码中包含有需要下层类加载加载的对象。使用java.lang.Thread类的getContextClassLoader()方法调用应用程序类加载器。
三、自定义类加载器
为什么要用?
- 想加载非classpath随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤:
- 继承ClassLoader父类
- 重写findClass方法(如果重写loadClass会破坏双亲委派机制)
- 读取类文件的字节码
- 调用父类的defineClass方法加载类
- 使用者调用该类加载器的
loadClass
方法
class MyClassLoader extends ClassLoader {
@Override // name 就是类名称
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "e:\\myclasspath\\" + name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}