JAVA双亲委派机制、不适用的场景及类加载过程
1 双亲委派
1.1 什么是双亲委派
The Java Class Loading Mechanism
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
详见 Understanding Extension Class Loading
Parents Delegation Model中一个类加载器收到类加载的请求,递归地把这个请求委派给父类加载器完成。当父加载器找不到指定的类时,子加载器尝试自己加载
1.2 类加载器的类别
1.2.1 类加载器类图
1.2.2 BootstrapClassLoader
BootstrapClassLoader(引导类加载器):用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path路径下的内容),是用原生代码(C语言)来实现的,并不继承自 java.lang.ClassLoader。
特点:
- c++编写
- 加载java核心库 java.*,如rt.jar中的类
- 加载ExtClassLoader和AppClassLoader,并指定他们的父类加载器
- 由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到引导类加载器的引用,所以不允许直接通过引用进行操作。
1.2.3 ExtClassLoader
ExtClassLoader (标准扩展类加载器)的特点:
- java编写
- 用来加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路径下的内容) 。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类
- 由sun.misc.Launcher$ExtClassLoader实现
1.2.4 AppClassLoader
AppClassLoader(系统类加载器)的特点:
- java编写
- 根据 Java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的
- 由sun.misc.Launcher$AppClassLoader实现
1.2.5 CustomClassLoader
CustomClassLoader(用户自定义类加载器)的特点:
- java编写
- 开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求
- 可加载指定路径的class文件
1.3 类查找优先列表
扩展框架使用了类加载委托机制。当运行时环境需要为应用程序加载一个新类时,它会按优先顺序在以下位置查找这个类:
- 引导【Bootstrap 】类: rt.jar中的运行时【runtime】类、i18n.jar中的国际化类以及其他类。
- 已安装的扩展:JRE的lib/ext目录中的JAR文件中的类,以及系统范围内、特定于平台的扩展目录中的类(例如Solaris™操作系统上的/usr/jdk/packages/lib/ext,但请注意,此目录的使用仅适用于Java™6和更高版本)。
- 类路径(CLASSPATH):在系统属性java.class.path指定的路径上的类,包括JAR文件中的类。如果类路径上的JAR文件具有具有Class-Path属性的清单,那么也将搜索由Class-Path属性指定的JAR文件。默认情况下,java.class.path属性的值是当前目录。可以使用 -classpath或-cp命令行选项或设置CLASSPATH环境变量来更改该值。命令行选项覆盖CLASSPATH环境变量的设置。
例如,优先级列表告诉您,只有在rt.jar、i18n.jar或安装的扩展中没有找到要加载的类时,才会搜索类路径。
1.4 类加载源码分析
- ClassLoader及其子类中的构造函数允许您在实例化新的类装入器时指定父类。如果没有显式指定父类,虚拟机的系统类装入器将被指定为默认父类。
- ClassLoader的loadClass方法
- 如果类已经装入,则返回该类。
- 否则,它将新类的搜索委托给父类装入器。
- 如果父类装入器没有找到该类,loadClass将调用findClass方法来查找并装入该类。
- ClassLoader的findClass方法如果父类装入器没有找到该类,ClassLoader的findClass方法将在当前类装入器中搜索该类。在应用程序中实例化类装入器子类时,可能需要重写此方法。
- 类java.net.URLClassLoader充当扩展和其他JAR文件的基本类装入器,覆盖java.lang.ClassLoader的findClass方法,以搜索一个或多个指定的url来查找类和资源
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个class是否已经加载过了
Class<?> c = findLoadedClass(name);
//如果类没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//如果有父加载器则让父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父加载器为空 则说明递归到bootStrapClassloader了
//使用bootStrapClassloader加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果类没有被,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
//记录类加载的一些统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
1.5 类委派加载流程图
1.6 双亲委派模型缺陷及不适用的场景
双亲委派模型很好的解决了各个类加载器的基础类统一问题(越基础的类由越上层的类加载器加载),基础类之所以称为基础,是因为它们总是作为被用户调用的API,但如果基础类又要回调用户的代码,那该怎么办呢。
假定类加载器 A 作为 B 的 parent,A 加载的类 对 B 是可见的; 然而 B 加载的类 对 A 却是不可见的。这是由 classloader 加载模型中的可见性(visibility)决定的。可见性原则允许子类加载器查看父ClassLoader加载的所有类,但父类加载器看不到子类加载器的类。
双亲委派模型不适用场景:
常见的 SPI 有 JDBC、JNDI、JAXP 等,这些 SPI 的接口由核心类库提供,却由第三方实现,这样就存在一个问题:
- SPI 的接口是 Java 核心库的一部分,由 BootstrapClassLoader 加载;并且接口的调用逻辑也是在核心库
- SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。
- 根据ClassLoader加载模型中的可见性原则,BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能将加载任务委派给 AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。
破坏双亲委派模型的典型示例:
- JNDI服务: JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI service provider interface)的代码,但启动类加载器不可能认识这些代码
- SPI 接口如 java.sql.DriverManager, java.sql.DriverManager 通过扫包的方式拿到指定的实现类,完成 DriverManager的初始化。问题来了,根据双亲委派的可见性原则,启动类加载器加载的 DriverManager 是不可能拿到系统应用类加载器加载的实现类
解决方案:
java设计团队引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。
这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器AppClassLoader
Java 在核心类库中定义了许多接口,并且还给出了针对这些接口的调用逻辑,然而并未给出实现。开发者要做的就是定制一个实现类,在 META-INF/services 中注册实现类信息,以供核心类库使用。
在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类(被线程上下文类加载器加载的类,能够被核心类库的调用方调用,且实现类不是通过双亲委派模型的方式加载,而是直接用传递的线程上下文类加载器加载)
SPI ServiceLoader 通过线程上下文获取能够加载实现类的classloader,一般情况下是 application classloader,绕过了双亲委派模型的层级限制,逻辑上打破了双亲委派原则
下面是DriverManager根据配置加载数据库驱动类的源码
//DriverManager.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;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// ServiceLoader.load
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文 classloader并非具体的某个loader,一般情况下是 application classloader, 但也可以通过 java.lang.Thread#setContextClassLoader 这个方法指定 classloader。
Thread.currentThread().getClassLoader() 和 Thread.currentThread().getContextClassLoader() 这两者除了在许多底层框架中取得的 ClassLoader 可能会有所不同外,其他大多数业务场景下都是一样的,只要知道它是为了解决什么问题而存在的即可。
1.7 双亲委派的作用
- 防止重复加载同一个类。父类加载到,子类不会重新加载,保证数据安全。
- 解决了各个类加载器的基础类统一问题
- 保证核心类不能被篡改。通过委托方式,不会去篡改核心类,保证了Class执行安全。
- 即使篡改也不会去加载
- 即使加载也不会是同一个类对象了。不同的加载器加载同一个类也不是同一个Class对象。
2 类加载
类加载 指将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象
Java中的类加载是一种懒加载的形式**。只有当这个类需要被使用的时候才会去加载,这样做的好处就是避免一次性加载全部,从而占用很大的内存。
2.1 类加载过程
在类加载的过程中,大致会经历以下7个阶段:加载、验证、准备、解析、初始化、使用、卸载。
- 加载:查找并且加载类的二进制数据
- 链接:
- 验证:确保被加载类的正确性 【检查输入的字节流。为了确保当前加载的Class字节流符合虚拟机的要求,不会危害到虚拟机】
- 准备:为类的静态变量分配内存,并将其初始化为默认值 【给类中静态变量赋予初值,其他的变量随着类的初始化,跟类存放在堆中】
- 解析:把类中的符号引用转换为直接引用 【将常量池中的符号引用变成直接引用】
- 初始化:执行构造方法 【初始化是类加载的最后一步,初始化阶段是执行类构造器 ()方法的过程】
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
2.2 类卸载
类卸载即该类的Class对象被GC。
卸载类需要满足3个要求:
- 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被GC
所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是自定义的类加载器加载的类是可能被卸载的。
相关文章
- 2011年全国软件大赛模拟题及参考答案(Java高职组)
- Java 开发环境配置--eclipse工具进行java开发
- 实例解析java + jQuery + json工作过程(获取JSON数据)
- 【Java】java扩展机制SPI 实现
- 这么说吧,java线程池的实现原理其实很简单
- java多线程的等待唤醒机制及如何解决同步过程中的安全问题
- Java实现 LeetCode 812 最大三角形面积 (暴力)
- java实现放麦子问题
- java实现第六届蓝桥杯打印大X
- java实现第六届蓝桥杯垒骰子
- Java实现第九届蓝桥杯等腰三角形
- (Java实现) 整数区间
- 【JAVA秒会技术之秒杀面试官】秒杀Java面试官——集合篇(一)
- java简单的MVC登入代码
- 【JAVA】java中split以"." 、""、“|”分隔字符串
- macos:安装java 17.0.6(android studio报错:Unable to locate a Java Runtime.)
- Java Web开发过程中隐藏很深的问题
- linux系统部署Java程序获取ip时报Caused by: java.net.UnknownHostException: XXXXXXXXXX: XXXXXXXXXX: Name or service not known
- Eclipse 报 “Exception in thread "main" java.lang.OutOfMemoryError: Java heap space ”错误的解决办法
- macos:安装java 17.0.6(android studio报错:Unable to locate a Java Runtime.)
- Atitit web httphandler的实现 java python node.js c# net php 目录 1.1. Java 过滤器 servelet1 1.2. Python的
- 求字符串A与字符串B的最长公共字符串(JAVA)
- Java 注解 Annotation自定义实战
- 一致性 Hash 算法 及Java 实现
- java.lang.OutOfMemoryError: Java heap space
- 【java】Java 抽象类
- 【java】Java线程池实现原理及业务中的实践