一、类加载器
- 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库(rt.jar)
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库,Java 虚拟机的实现会提供一个扩展库目录(%JAVA_HOME%lib/ext/*.jar)
-
应用类加载器(application class loader):它根据类路径来加载某个类 (CLASSPATH)
-
自定义加载器:加载自定义路径的类
1.AppClassLoader
二、示例
1. 类加载
public class ClassLoaderTree { public static void main(String[] args) { ClassLoader loader = ClassLoaderTree.class.getClassLoader(); while (loader != null) { System.out.println(loader.toString()); loader = loader.getParent(); } } }
结果
sun.misc.Launcher$AppClassLoader@9304b1 //子
sun.misc.Launcher$ExtClassLoader@190d11 //父
2.
private static ClassLoader getLocaleClassLoader(File libDir) throws Exception { List<URL> urls = new ArrayList<>(); // 获取所有的jar文件 File[] jarFiles = libDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".jar"); } }); // 将jar文件路径写入集合 for (File jarFile : jarFiles) { urls.add(jarFile.toURI().toURL()); } // 实例化类加载器 return new URLClassLoader(urls.toArray(new URL[urls.size()])); } public static void main(String[] args) throws Exception{ String name = "com.google.common.collect.ImmutableList"; ClassLoader classLoader = getLocaleClassLoader(new File("/Users/gl/IntelliJProjects/JBase/kafka/build/dependency/")); Class clazz = classLoader.loadClass(name); System.out.println(clazz); //ClassLoader 为 null, 原因是: //Cat.class.getClass()是java.lang.Class对象, 它的加载器是引导类加载器 Cat.class.getClass().getClassLoader(); //下面是ClassNotFoundException clazz = Cat.class.getClassLoader().loadClass(name); System.out.println(clazz); }
三、类加载器的代理模式
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。
代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用java.lang.Object
类,也就是说在运行的时候,java.lang.Object
这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object
类,而
且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的
Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同
类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到,后面会详细
介绍。
四、加载类的过程
在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启
动这个加载过程的类加载器,有可能不是同一个。
真正完成类的加载工作是通过调用 defineClass
来实现的;而启动类的加载过程是通过调用 loadClass
来实现的。前者称为一个类的定义加载器(defining loader),
后者称为初始加载器(initiating loader)。
在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。
两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。
如类 Outer
引用了类Inner
,则由类 Outer
的定义加载器负责启动类Inner
的加载过程。
方法 loadClass()
抛出的是 java.lang.ClassNotFoundException
异常;
方法 defineClass()
抛出的是 java.lang.NoClassDefFoundError
异常。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class
类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会
尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass
方法不会被重复调用。
五、线程上下文类加载器
java.lang.Thread
中的set/get ContextClassLoader, 如果没有设置ContextClassLoader 的话,线程将继承其父线程的类加载器。初始
线程的类加载器是系统类加载器。
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider
Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,
如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers
包中。
SPI 接口中的代码经常需要加载具体的实现类,如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory
类中的 newInstance()
方法用来生成一个
新的 DocumentBuilderFactory
的实例。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而问题在于:
1. SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的
2. SPI 的实现是由应用类加载器来加载的
六、类加载器与 Web 容器
首先尝试去加载某个类,如果找不到再代理给父类加载器。其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例
外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。