文章目录
- 类加载器
- findClass
- loadClass
- JDK双亲委派的破坏
- 第一次破坏
- 第二次破坏
- tomcat
- 热部署
- JDK9最新改动
双亲委派机制是当类加载器需要加载某一个.class字节码文件时,则首先会把这个任务委托给他的上级类加载器,递归这个操作,如果上级没有加载该.class文件,自己才会去加载这个.class。主要目的是为了安全,保证一个Java类在JVM的唯一性
类加载器
- Bootstrap ClassLoader(启动类加载器):该类加载器由C++实现的。负责加载Java基础类,对应加载的文件是%JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar和class等。
- Extension ClassLoader(标准扩展类加载器):继承URLClassLoader。对应加载的文件是%JRE_HOME/lib/ext 目录下的jar和class等。
- App ClassLoader(系统类加载器):继承URLClassLoader。对应加载的应用程序classpath目录下的所有jar和class等。
- CustomClassLoader(用户自定义类加载器):由Java实现。我们可以自定义类加载器,并可以加载指定路径下的class文件
类加载器的父子关系不是通过继承来实现的,比如AppClassLoader并非ExtClassLoader的子类,只是AppClassLoader的parent指向ExtClassLoader对象。
所以若自定义类加载器,不是去继承AppClassLoader,而是继承ClassLoader抽象类,再重写findClass和loadClass即可
findClass
作用是根据类的路径信息,查找类,再调用defineClass 把类字节数组生成class,如果需要自定义ClassLoader的话,必须重写该方法。不打破双亲委派机制
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;
}
loadClass
加载类,父类不能加载时,由当前的类加载器加载,会调用findClass方法
若要打破双亲委派机制,重写loadClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//检查指定类是否被当前类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) { //如果没被加载过,委派给父加载器加载
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); //委托父加载器
} else {
c = findBootstrapClassOrNull(name); //ext类加载器加载器才会走到该分支,尝试让最顶层的BootstraptClassLoader加载
}
} catch (ClassNotFoundException e) {
//如果父加载器无法加载,会抛出 ClassNotFoundException
}
if (c == null) {
// 父类不能加载,由当前的类加载器加载
long t1 = System.nanoTime();
c = findClass(name); //需要自己加载类时,调用findClass
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) { //如果要求立即链接,那么加载完类直接链接
resolveClass(c);
}
return c;
}
}
JDK双亲委派的破坏
第一次破坏
在 jdk 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类,所以已经有人继承这个抽象类,重写 loadClass 方法来实现用户自定义类加载器。
而在 1.2 的时候要引入双亲委派模型,为了向前兼容, loadClass 这个方法还得保留着使之得以重写,新搞了个 findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass。
这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在 loadClass 上,但是又允许重写 loadClass,重写了之后就可以破坏委派逻辑了
第二次破坏
第二次破坏指的是 JNDI、JDBC 之类的情况。
首先得知道什么是 SPI(Service Provider Interface),它是面向拓展的,也就是说我定义了个规矩,就是 SPI ,具体如何实现由扩展者实现。
像我们比较熟的 JDBC 就是如此。
MySQL 有 MySQL 的 JDBC 实现,Oracle 有 Oracle 的 JDBC 实现,我 Java 不管你内部如何实现的,反正你们这些数据库厂商都得统一按我这个来,这样我们 Java 开发者才能容易的调用数据库操作,所以在 Java 核心包里面定义了这个 SPI。
而核心包里面的类都是由启动类加载器去加载的,但它的手只能摸到\lib或Xbootclasspath指定的路径中,其他的它鞭长莫及。
而 JDBC 的实现类在我们用户定义的 classpath 中,只能由应用类加载器去加载,所以启动类加载器只能委托子类来加载数据库厂商们提供的具体实现,这就违反了自下而上的委托机制。
具体解决办法是搞了个线程上下文类加载器,通过setContextClassLoader()默认情况就是应用程序类加载器,然后利用Thread.current.currentThread().getContextClassLoader()获得类加载器来加载。
这就是第二次破坏双亲委派模型
tomcat
打破双亲委派
热部署
为了满足热部署的需求,进行不停机更新。
OSGI (开放服务网关协议,Open Service Gateway Initiative)技术是Java动态化模块化系统的一系列规范。
OSGI 就是利用自定义的类加载器机制来完成模块化热部署,而它实现的类加载机制就没有完全遵循自下而上的委托,有很多平级之间的类加载器查找。
JDK9最新改动
JDK 9 为了模块化的支持,对双亲委派模式做了一些改动:
- 扩展类加载器被平台类加载器(Platform ClassLoader)取代。
JDK 9 时基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个 JMOD 文件), 其中的 Java
类库就已天然地满足了可扩展的需求,那自然无须再保留 <JAVA_HOME>\lib\ext 目录,此前使用这个目录或者 java.ext.dirs 系统变量来扩展 JDK 功能的机制已经没有继续存在的价值了。
- 平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader。
现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。
如果有程序直接依赖了这种继承关系,或者依赖了 URLClassLoader 类的特定方法,那代码很可能会在 JDK 9 及更高版本的 JDK 中崩溃。
- 启动类加载器现在是在 Java 虚拟机内部和 Java 类库共同协作实现的类加载器(以前是 C++实现)。
为了与之前的代码保持兼容,所有在获取启动类加载器的场景(譬如 Object.class.getClassLoader)中仍然会返回 null来代替,而不会得到 BootClassLoader 的实例。
- 类加载的委派关系也发生了变动。
当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
在 Java 模块化系统明确规定了三个类加载器负责各自加载的模块:
- 启动类加载器负责加载的模块
java.base java.security.sasl
java.datatransfer java.xml
java.desktop jdk.httpserver
java.instrument jdk.internal.vm.ci
java.logging jdk.management
java.management jdk.management.agent
java.management.rmi jdk.naming.rmi
java.naming jdk.net
java.prefs jdk.sctp
java.rmi jdk.unsupported
- 平台类加载器负责加载的模块
java.activation* jdk.accessibility
java.compiler* jdk.charsets
java.corba* jdk.crypto.cryptoki
java.scripting jdk.crypto.ec
java.se jdk.dynalink
java.se.ee jdk.incubator.httpclient
java.security.jgss jdk.internal.vm.compiler*
java.smartcardio jdk.jsobject
java.sql jdk.localedata
java.sql.rowset jdk.naming.dns
java.transaction* jdk.scripting.nashorn
java.xml.bind* jdk.security.auth
java.xml.crypto jdk.security.jgss
java.xml.ws* jdk.xml.dom
java.xml.ws.annotation* jdk.zipfs
- 应用程序类加载器负责加载的模块
jdk.aot jdk.jdeps
jdk.attach jdk.jdi
jdk.compiler jdk.jdwp.agent
jdk.editpad jdk.jlink
jdk.hotspot.agent jdk.jshell
jdk.internal.ed jdk.jstatd
jdk.internal.jvmstat jdk.pack
jdk.internal.le jdk.policytool
jdk.internal.opt jdk.rmic
jdk.jartool jdk.scripting.nashorn.shell
jdk.javadoc jdk.xml.bind*
jdk.jcmd jdk.xml.ws*
jdk.jconsole