概述
我们都知道jvm加载类为了安全考虑,采用的是双亲委派机制,但是在一些场景下我们仍然向打破双亲委派。常见的场景有热部署,tomcat等。
双亲委派加载
双亲委派加载的流程简述:当classloader加载类时,先会查询是否已加载过,若加载过,直接返回;没有的话,就向父加载器传递,一直到bootstrap加载器,bootstrap判断是否可以加载,若可以的话,直接加载并返回,若不可以的话,则向下传递。详细可参考此文Class文件如何加载到JVM。
先看下双亲委派的源码:
public abstract class ClassLoader {
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) {
long t0 = System.nanoTime();
try {
//缓存中没查到,则请求父类尝试加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//最顶级父类bootstrapClassLoader加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//父类中既没有缓存,也无法加载,则当前classloader尝试加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//自定义classLoader只能重写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;
}
}
}
loadClass是protected方法,那其实很简单,想要打破双亲委派机制的话,只要重写loadClass方法即可。
一般我们自定义类加载器,不需要打破双亲委派的话,只需要重写findClass方法
打破双亲委派
我们先写个demo来看看如何实现自定义类加载器
public class CustomFindClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File("/home/yuhan/Documents/workspace/" + replace(name) + ".class");
if (!file.exists()) {
return super.findClass(name);
}
FileInputStream inputStream = null;
ByteArrayOutputStream outputStream = null;
try {
inputStream = new FileInputStream(file);
outputStream = new ByteArrayOutputStream();
int b = 0;
while ((b = inputStream.read()) != 0) {
outputStream.write(b);
}
byte[] bytes = outputStream.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
} catch (Exception e) {
}
}
return super.findClass(name);
}
private String replace(String className) {
return className.replaceAll("\.", "/");
}
public static void main(String[] args) throws ClassNotFoundException {
CustomFindClassLoader customFindClassLoader = new CustomFindClassLoader();
Class first = customFindClassLoader.loadClass("com.roc.jvm.TestClass");
System.out.println(first.getClassLoader());
customFindClassLoader = new CustomFindClassLoader();
Class second = customFindClassLoader.loadClass("com.roc.jvm.TestClass");
System.out.println(second.getClassLoader());
System.out.println(first == second);
}
}
执行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
true
我重写了findClass方法,然后尝试取加载类,通过结果发现,并不会使用自定义加载器加载,而是使用了AppClassLoader进行加载,并且加载的两个class是相同的。这就说明重写findClass方法是不会打破双亲委派的。
public class CustomLoadClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
File file = new File("/home/yuhan/Documents/workspace/" + replace(name) + ".class");
if (!file.exists()) {
return super.loadClass(name);
}
FileInputStream inputStream = null;
ByteArrayOutputStream outputStream = null;
try {
inputStream = new FileInputStream(file);
outputStream = new ByteArrayOutputStream();
int b = 0;
while ((b = inputStream.read()) >= 0) {
outputStream.write(b);
}
byte[] bytes = outputStream.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
} catch (Exception e) {
}
}
return super.loadClass(name);
}
private String replace(String className) {
return className.replaceAll("\.", "/");
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return super.findClass(name);
}
public static void main(String[] args) throws ClassNotFoundException {
CustomLoadClassLoader customClassLoader = new CustomLoadClassLoader();
Class first = customClassLoader.loadClass("com.roc.jvm.TestClass");
System.out.println(first.getClassLoader());
customClassLoader = new CustomLoadClassLoader();
Class second = customClassLoader.loadClass("com.roc.jvm.TestClass");
System.out.println(second.getClassLoader());
System.out.println(first == second);
}
}
执行结果:
com.roc.jvm.CustomLoadClassLoader@1218025c
com.roc.jvm.CustomLoadClassLoader@87aac27
false
重写了loadClass方法之后,就打破了双亲委派机制,使用了自定义加载器进行加载,而不是向上询问了,并且这是两个不同的class对象,同时这个例子也很好的解释了热部署的原理。
热部署
使用较新版本的idea会发现,settings里有个功能叫做hot swap,可以在项目启动后,对有改动的文件进行reload,就可以实现热部署功能,而无需重新启动。
在debug模式下,idea会使用两个加载器,一个用来加载例如第三方jar包等不变的类,另一个RestartClassLoader加载项目里的类。这样当某个java文件被修改之后,可以通过创建新的RestartClassLoader,将修改后的文件加载到内存中。
原理猜测
前提:类的唯一性由类加载器实例和类的全限定名一同确定
当修改后的类被新的类加载器加载后,肯定是两个不同的class对象,内存中class的引用指向了新加载的class对象
Tomcat
tomcat的类加载机制是打破双亲委派的,tomcat的类加载机制如下:
(图片来源于网络)
JSP ClassLoader:用于jsp文件修改后的热重载
WebApp ClassLoader:各个webApp私有的类加载器,加载的class只对当前webApp可见
Shared ClassLoader:各个webApp共享的类加载,加载的class对所有webApp可见
Catalina ClassLoader:Tomcat容器私有的类加载器,对webApp不可见
Common ClassLoader:Tomcat最基础的类加载器,webApp和Tomcat均可见
原因:tomcat可以同时部署多个应用,每个应用可能存在相同的类库(使用Shared ClassLoader加载),同一类库的不同版本及各自的代码(WebApp ClassLoader),Tomcat本身所依赖的类库(Catalina ClassLoader),如果希望WebApp和Tomcat两者依赖的类库共享,那么就使用Common ClassLoader。
关于spi
JNDI ,JDBC 等都是 Java 的标准服务,基本都是由Bootstrap ClassLoader进行加载的。例如DriverManager.class,但是Driver是由第三方厂商根据JDBC的标准实现的,例如mysql,oracle等,Bootstrap ClassLoader是无法加载的,Jvm团队引入了Context ClassLoader进行加载,默认Context ClassLoader是AppClassLoader。
众所周知:核心类都由Bootstrap ClassLoader加载,而核心类的依赖类也应该由 Bootstrap ClassLoader加载,而JDBC这里DriverManager.class由Bootstrap ClassLoader加载,而其依赖类Driver.class因由第3方厂商实现,而无法由Bootstrap ClassLoader加载。在这里可以理解为是打破了双亲委派的。Driver.class实际是由App ClassLoader加载,而App ClassLoader加载class是符合双亲委派的
我们自定义加载器,是否可以重新加载String.class?
不可以,JVM设计者对于加载核心类设置了权限控制,核心类只能由Bootstrap ClassLoader加载。在加载时,判断如果以java开头,便会直接抛出异常
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}