1.什么是双亲委派
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// -----??-----
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) {
//非bootstrapclassload,调用父类的loadclass
c = parent.loadClass(name, false);
} else {
//parent为null说明是bootstrapclassload
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//没找到class,说明没被加载过,尝试加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//具体实现是URLClassLoader中的findclass,
c = findClass(name);
// 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;
}
}
URLClassloader中的findclass方法:
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
//这是一个native方法,
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
//Extclassloader直接返回null,appclassload返回非null
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//查找并定义class
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;
}
URLClassLoader中调用的是父类Classloader的defineclass:
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
URL url = res.getCodeSourceURL();
if (i != -1) {
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// Now read the class bytes and define the class
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
//这里定义class,内部会调用一个native方法
return defineClass(name, b, 0, b.length, cs);
}
}
综上总结下:
- bootstrapClassLoader 只加载
jre/lib下的包 - extClassLoader只加载
jre/lib/ext下的包 - appClassLoader只加载
classpath下的包 - ClassLoader抽象类中有三个重要方法
loadclass,findclass(由子类URLClassLoader具体实现,ext和app都继承URLClassLoader),以及defineclass - 自定义classloader时需要重写
loadclass(打破双亲委派)以及findclass(自己加载类)方法 - 不同类加载器实例加载同一个限定名的类, 这两个类依旧是不一样的
2.为什么要破坏双亲委派
2.1 历史原因
现有loadclass接口,后有的双亲委派, 在双亲委派出现之前,有很多classload自定义loadclass,所以双亲委派为了兼容就留下了可以重写loadclass的口子(破坏双亲委派需要改造loadclass方法)
2.2 spi机制
spi机制是服务端提供接口,由客户端取实现,比如JDBC的driver类
加载driver类的classloader是BootstrapClassLoader, 当加载driver类时,其实现按道理也应该由BootstrapClassLoader加载才会满足双亲委派,但是实际这是不可能的, 所以DriverManager.getconnection时默认会委派appclassloader来加载第三方驱动
双亲委派是向上而行的,发起人是下层classloader,当上层无法加载时才会由发起人进行加载
DriverManager类是bootstrapClassLoader加载,是上层发起,需要加载第三方类,所以只能委派给底层的appclassloader,故破坏了双亲委派
2.3 热替换,热部署/多发布包隔离
双亲委派中一个类只能被一个类加载器加载一次,当类被修改时,是无法被再次加载的
所以如果要实现热部署,就一定要重定义一个classloader,自己去重新加载一个类
tomcat中会同时运行多个war包工程,这些工程按道理应该是隔离的,但当这些war包都包含同一个jar依赖包的不同版本(也就是说有相同的全限定名的类,这些类可能一致可能不一致),如果不打破双亲委派,这些类只能存在一份,多个war包工程就不隔离了,所以需要自定义classloader,打破双亲委派
3.自定义类加载器
3.1 不打破双亲委派
重写classloader的findclass方法,这种方式只能加载classpath外的class文件
加载流程依旧符合双亲委派, 一级一级向上查看myclassLoader->appclassloader->extclassloader->bootstrapclassloader
public class MyClassLoader extends ClassLoader {
//自定义加载路径,在这个路径下使用自定义类加载器
private String path;
public MyClassLoader(String path, ClassLoader parent) {
super(parent);
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//加载文件进内存
byte[] b = loadClassData(name);
//调用classload二的defineclass
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private byte[] loadClassData(String name) throws IOException {
name = path + name + ".class";
InputStream is = null;
ByteArrayOutputStream outputStream = null;
try {
is = new FileInputStream(new File(name));
outputStream = new ByteArrayOutputStream();
int i = 0;
while ((i = is.read()) != -1) {
outputStream.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
outputStream.close();
}
if (is != null) {
is.close();
}
}
return outputStream.toByteArray();
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//这个路径下以及classpath路径下都有HelloWorld;
MyClassLoader myClassLoader = new MyClassLoader("E:\\code-repo\\OTHER\\test\\file\\", MyClassLoader.class.getClassLoader());
Object client = myClassLoader.loadClass("HelloWorld").newInstance();
//输出false, 因为不是一个类加载器加载的
System.out.println(client instanceof HelloWorld);
}
3.2 打破双亲委派
重写classloader的loadclass和findclass方法
@Override
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) {
/**
* 这部分逻辑可以自定义
* 可以先向上委派,再由自己加载
* 也可以根据包名来判断是否先由自己加载,再向上委派
* */
try {
c = this.getParent().loadClass(name);
} catch (ClassNotFoundException e) {
}
if (c == null) {
c = findClass(name);
}
// if (!name.startsWith("自定义包名")) {
// c = this.getParent().loadClass(name);
// } else {
// c = findClass(name);
// }
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
4. tomcat中的类加载器
4.1 tomcat类加载器结构图
- Bootstrap 这个类加载器包含 Java 虚拟机提供的基本运行时类,以及系统扩展目录$JAVA_HOME/jre/lib/ext) 中存在的 JAR 文件中的任何类。 注意:一些 JVM 可能将其实现为多个类加载器,或者它可能根本不可见(作为类加载器)。实际就是bootstrapclassloader和extclassloader
- System 这个类装入器通常是从CLASSPATH环境变量的内容初始化的。实际就是appclassloader.所有这些类对Tomcat内部类和web应用程序都是可见的。但是,标准的 Tomcat 启动脚本($CATALINA_HOME/bin/catalina.sh 或 %CATALINA_HOME%\bin\catalina.bat)完全忽略了 CLASSPATH 环境变量本身的内容,而是从以下存储库构建系统类加载器:
- $CATALINA_HOME/bin/bootstrap.jar 包含用于初始化Tomcat服务器的main()方法,以及它所依赖的类装入器实现类。
- CATALINA_HOME/bin/tomcat-juli.jar 日志实现类。其中包括
java.util.loggingAPI的增强类,称为Tomcat JULI,以及Tomcat内部使用的Apache Commons Logging库的重命名副本。 - $CATALINA_HOME/bin/commons-daemon.jar 来自Apache Commons Daemon项目的类。这个JAR文件不在catalina.bat|.sh脚本构建的CLASSPATH中,而是从bootstrap.jar的清单文件中引用。
- Common 加载
common.loader(conf/catalina.properties)下的classes文件、资源文件和jar包,这些类对Tomcat内部类和所有Web应用程序都是可见的。 - Server 加载
server.loader(conf/catalina.properties)下的classes文件、资源文件和jar包, 只对Tomcat内部可见,而对web应用程序完全不可见。 - Shared 加载
shared.loader(conf/catalina.properties)下的classes文件、资源文件和jar包,对所有web应用程序可见,并可用于在所有web应用程序之间共享代码。但是,对此共享代码的任何更新都需要Tomcat重新启动 - WebappX 单个Tomcat实例中的每个Web应用程序创建的类加载器。加载Web应用程序的/Web-INF/Class目录中的所有未打包类和资源,再加上Web应用程序/Web-INF/lib目录下JAR文件中的类和资源,对此web应用程序都是可见的,而对其他类则不可见。