类加载(三)
一、类加载器
- 引导类加载器(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 核心库的类型安全。

浙公网安备 33010602011771号