学习参考的文章链接
1.类加载机制器
classLoader的作用
- 把class文件的二进制数据流读入jvm内部,交给jvm来链接和初始化
class文件的显式加载和隐式加载
- 显式加载直接使用Class.forName进行加载,并能够初始化类,或者是this.getClass().getClassLoader().loadClass()
- 隐式加载,比如通过new对象,那么这种也会自动把类加载进来。虚拟机会自动把类加载进来
2.类加载器的类型
启动类加载器
- c/c++实现
- 加载java核心类库
- 不继承classLoader,没有父类
- 为了安全只会加载java,javax,sun开头的包里面的类
- 加载扩展类和系统类加载器,并且为他们指定父类加载器
扩展类加载器
- java的sum.music.Launcher$ExtClassLoader
- 继承ClassLoader,父类加载器是启动类加载器
- jre/lib/ext目录下面加载类
应用类加载器
- 父类加载器是ExtClassLoader
- 负责加载classPath下面的类,程序的默认加载器
用户自定义加载器
- 可以加载网络资源上面的远程资源
- 自定义方法
- 重写loadClass,但是不推荐,因为需要遵循双亲委派机制
- findClass推荐,获取字节流,然后交给defineClass来生成类对象
自定义ClassLoader
自定义的classLoader如果没有设定父类加载器,那么父类加载器是谁?
- 只要是继承了ClassLoader那么就会调用构造方法,并且使用getSystemClassLoader(),也就是系统类加载器,其实就是AppClassLoader
- 既然是AppClassLoader那么自然就能够获取到jre/lib目录和jre/lib/ext上面的类。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
//把自定义类加载器的父类加载器设置为SystemClassLoader
this.parent = parent;
.../
}
如果自定义类加载器强制parent设置为null,那么是不是就无法加载类?
- 首先就算parent设置为null,但是仍然会能够加载lib下面的类,通过loadClass的双亲委派最后还是能够访问到bootstrap类加载器
自定义ClassLoader
自定义的classLoader如果没有设定父类加载器,那么父类加载器是谁?
- 只要是继承了ClassLoader那么就会调用构造方法,并且使用getSystemClassLoader(),也就是系统类加载器,其实就是AppClassLoader
- 既然是AppClassLoader那么自然就能够获取到jre/lib目录和jre/lib/ext上面的类。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
//把自定义类加载器的父类加载器设置为SystemClassLoader
this.parent = parent;
.../
}
如果自定义类加载器强制parent设置为null,那么是不是就无法加载类?
- 首先就算parent设置为null,但是仍然会能够加载lib下面的类,通过loadClass的双亲委派最后还是能够访问到bootstrap类加载器
总结
最后需要区分的一点就是启动类加载的加载器不一定就是加载类的加载器,而是最后调用defineClass的那个加载器才是最后加载类的那个加载器。
3.ClassLoader源码分析
- UrlClassLoader重写了findClass
- AppClassLoader重写了loadClass方法(但实际上还是调用的是ClassLoader的,遵循双亲委派)
ClassLoader主要方法解析
- public final ClassLoader getParent()获取父类加载器
- public Class<?> loadClass(String name)双亲委派的体现,加载类
- protected Class<?> findClass (String name)也是查找类, 但是直接就是本类加载器去查找与加载。会在loadClass上面查找父类失败的时候调用
- resloveClass主要就是把符号引用解析为直接引用
protected Class<?> loadClass(String name, boolean resolve)
//resolve如果是true就需要进行解析操作
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);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果父类加载器没有加载
if (c == null) {
//当前类加载器去加载这个类
long t1 = System.nanoTime();
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、findResource
- 还拓展了URLClassPath来获取class的字节流
Class.forName和ClassLoader.loadClass的区别
- Class.forName加载并初始化。而且forName调用的加载器是,调用这个静态方法的那个类的类加载器来加载的。
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
//获取调用者的类加载器
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
- loadClass就只能是加载类,但是并不会初始化。
4.双亲委派的弊端
- 顶层的ClassLoader无法访问下层的ClassLoader的类
如何解决双亲委派的弊端
- 打破双亲委派机制,委托线程上下文加载器去加载其它SPI的核心类
- 还有就是代码热替换和、模块化部署
5.沙箱安全机制
- 如果自定义一个String类,那么仍然还是不能覆盖掉之前的String,因为jvm会通过沙箱保护机制来保护String,就算有这个自定义的类,那么通过双亲委派会优先加载rt.jar包下面的String
6.Tomcat类加载机制
- 从tomacat的conf文件中的conf/catalina.properties中可以看到它的自定义类加载器
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
- Common以应用类加载器为父类,tomcat的顶层公用加载器
- Catalina:Tomcat容器的私有类加载器对webapp不可见,路径可以通过server.loader=来指定
- Shared:Common为父类,所有webapp的父类加载器
- Web应用:以Shared为父类,加载/WEB-INF/classes下面的class和/WEB-INF/lib下面的jar包
Tomcat的执行顺序
- bootstrap引导类加载器加载,加载的是bin目录下面的jvm所需要的启动类和/jre/lib/ext拓展包里面的类库
- system系统类加载器加载,加载的是lib目录下的tomcat所需要的启动类
- 应用类加载/WEB-INF/classes再加载/WEB-INF/lib,其实就是每个web应用的类加载器
- Common类加载CATALINA_HOME/lib
- 下面的ClassLoader(tomcat的)的loadClass源码能够分析出来就是按照这个顺序来进行加载,而且可以手动设置为采用默认类记载器进行加载。
那么tomacat是不是违背了双亲委派
- 看看这个加载顺序就知道肯定是了,因为正常来说是Common先加载的,但是现在却先变成应用类加载器先去加载类路径的类和lib包下面的类。
- 实现了隔离性,每个webApp能够通过这样的加载机制加载不同版本的jar包和不同的SPI。
Bootstrap(Tomcat的引导类加载器)
- 先查看这个bootstrap是不是空,如果是空那么就创建一个并且复制给daemon,并且调用init,如果是空的那么就把catalinaLoader设置给线程上下文类加载器
- init这个方法其实也是把catalinaClassLoader赋值给线程上下文加载器(容器的类加载器就是catalinaClassLoader)
- init之后就会调用initClssLoaders,然后这里就会发现他们就是在初始化对应的类加载器,包括catalinaClassLoader,shareClassLoader、serverClassLoader
- 最后就是createClassLoader() ,使用的就是catalina.properties下面的三个loader。
public static void main(String[] args) {
synchronized(daemonLock) {
if (daemon == null) {
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable var5) {
handleThrowable(var5);
var5.printStackTrace();
return;
}
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
}
public void init() throws Exception {
//创建tomcat三个类加载器
this.initClassLoaders();
//线程上下文类加载器设置为catalinaLoader,也就是容器的类加载器
Thread.currentThread().setContextClassLoader(this.catalinaLoader);
SecurityClassLoad.securityClassLoad(this.catalinaLoader);
....
}
private void initClassLoaders() {
try {
//创建tomcat的三个重要的classLoader
this.commonLoader = this.createClassLoader("common", (ClassLoader)null);
if (this.commonLoader == null) {
this.commonLoader = this.getClass().getClassLoader();
}
this.catalinaLoader = this.createClassLoader("server", this.commonLoader);
this.sharedLoader = this.createClassLoader("shared", this.commonLoader);
} catch (Throwable var2) {
handleThrowable(var2);
log.error("Class loader creation threw exception", var2);
System.exit(1);
}
}
//刚好加载了三个类都是之前配置文件上面的,那三个
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if (value != null && !value.equals("")) {
value = this.replace(value);
List<Repository> repositories = new ArrayList();
String[] repositoryPaths = getPaths(value);
String[] arr$ = repositoryPaths;
int len$ = repositoryPaths.length;
for(int i$ = 0; i$ < len$; ++i$) {
...//解析并且获取类加载器下面的路径
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
} else {
return parent;
}
}
ClassLoader的加载过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wZKNPdAd-1635326345834)(…/…/…/…/…/AppData/Roaming/Typora/typora-user-images/image-20211027152508382.png)]
- 这个方法是WebappClassLoaderBase实现的,但是调用这个方法的是ParallelWebAppClassLoader(相当于就是webapp的类加载器)。
- 整个过程是先从自己的缓存和父类的缓存中加载类
- 如果没有那么就通过bootstrap类加载器来进行加载。怎么看出来?就是从javaseClassLoader赋值上,也就是WebappClassLoaderBase的构造器上面,它是通过String.class的类加载器来进行加载,而String.class的类加载器就是Bootstrap。
- (委托默认是flase,也就是每次都是先让本类加载器先去找,也就符合上面的tomcat加载图)如果不需要那么就直接通过webapp自己来进行加载,而且如果没有加载到,那么就会委托父类去加载这个类。
那么整个源码能够看出来jvm的类加载的方式。
- 先从缓存中找
- 接着就是从bootstrap来先加载这个类
- 然后就是通过判断delegate(true是使用双亲委派,直接通过父类去加载),false就是直接通过本类先去加载类,默认是false也就是本类直接先去加载
- 最后才是通过本类的父类加载器(Common类加载器)去记载
那么这里就会问到为啥要这样干啊?直接双亲委派就完事了?
原因就是直接双亲委派就不能够让web应用的不同版本第三方库共存,只能允许一个存在,因为父类加载器只会加载一次这个类。而类是否相同的唯一鉴别标准就是加载类的加载器是不是相同,如果不相同那么就两个类就算类名同最后实际上是不同的。也就是说不同类加载器加载类都是不同的类,这样就解决了tomcat的隔离问题(web容器与web应用还有就是web应用之间的隔离)。
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
if (log.isDebugEnabled()) {
log.debug("loadClass(" + name + ", " + resolve + ")");
}
Class<?> clazz = null;
this.checkStateForClassLoading(name);
//先从自己缓存中查找
clazz = this.findLoadedClass0(name);
if (clazz != null) {
...
}
//然后再从parent的缓存查找是否存在这个类
clazz = this.findLoadedClass(name);
if (clazz != null) {
.....
}
//如果缓存没有先从system类的加载器来加载。
String resourceName = this.binaryNameToPath(name, false);
ClassLoader javaseLoader = this.getJavaseClassLoader();
//是否需要parent来进行代理
boolean delegateLoad = this.delegate || this.filter(name, true);
//如果是true那么就是父类加载器先去加载
if (delegateLoad) {
label211: {
if (log.isDebugEnabled()) {
log.debug(" Delegating to parent classloader1 " + this.parent);
}
try {
//通过父类先去加载
clazz = Class.forName(name, false, this.parent);
....
} catch (ClassNotFoundException var16) {
break label211;
}
return var10000;
}
}
....
//如果父类记载失败那么就调用自身的加载
try {
clazz = this.findClass(name);
if (clazz != null) {
.....
var10000 = clazz;
return var10000;
}
} catch (ClassNotFoundException var17) {
}
....
//如果本类加载器找不到就会委托父类加载器去加载。
try {
clazz = Class.forName(name, false, this.parent);
if (clazz != null) {
....
var10000 = clazz;
return var10000;
}
} catch (ClassNotFoundException var14) {
}
throw new ClassNotFoundException(name);
}
protected WebappClassLoaderBase() {
super(new URL[0]);
this.state = LifecycleState.NEW;
ClassLoader p = this.getParent();
if (p == null) {
p = getSystemClassLoader();
}
this.parent = p;
//获取String的classLoader
ClassLoader j = String.class.getClassLoader();
if (j == null) {
for(j = getSystemClassLoader(); j.getParent() != null; j = j.getParent()) {
}
}
//给javaseClassLoader赋值,赋值的内容就是bootstrap类加载器
this.javaseClassLoader = j;
this.securityManager = System.getSecurityManager();
if (this.securityManager != null) {
this.refreshPolicy();
}
}
Tomcat破坏双亲委派机制的面试题
既然Tomcat不遵循双亲委派机制,那么如果我自己定义一个恶意的HashMap,会不会有风险呢?
- 经过上面的分析,很明显并不会,这里只不过是加载类的类加载器的执行顺序不同而已,违反双亲委派,但是不代表就不会调用到上层的classLoader来保护基本类库。
- 回忆加载顺序,bootstrap之后是system这样做的目的就是为了能够优先加载核心库里面的类(包括HashMap),但是tomcat的目的只是为了让那些SPI不同版本能够被加载,所以才会有下面的自定义WebApp的类加载器。Common是为了统一那些版本一致的SPI,第三方库。所以就算webapp不先去请求common的,但是还是会去找bootstrap的保证核心类库的安全。
Tomcat是个web容器,那么它要解决什么问题?
- web容器需要加载多个webapp,这个时候不同的webapp就有可能是采用不同版本的第三方库,这个时候就需要通过隔离两个应用之间的类加载机制。如果是像之前一样通过默认类加载器,那么一个类只可能会加载一次,也就是不可能会有其他版本,所以这个时候就需要打破双亲委派机制,回忆上面tomcat的类加载顺序,先bootstrap,然后就是system,接着就跳到了webapp类加载器加载,最后才是common类加载器加载。
- web容器里面也有的库版本相同需要共享,那么tomcat就需要解决这些库共享问题,防止相同的库被加载多次
- web容器也有自己的类库,需要和webapp应用的进行一个隔离。所以这里就会有
- jsp编译过来也是一个class文件需要被加载,但是jsp修改之后class名是不会改变的,举个例子,a.jsp,编译过来加入到类路径,但是我修改a.jsp内容之后再次编译也还是名字没有改变,如果使用默认类加载器那么就只能够加载第一次,如果需要实现加载多次就需要专门去加载jsp的类加载器。一个jsp文件对应一个类加载器,jsp修改,那么类加载器删除重新创建一个,然后加载新的jsp完成动态化。
- 解决第一个问题和第三个问题的关键就是Catalina和shared的分开,catalina负责web容器,shared负责webapp的。而WebApp之间的同级也是有多个类加载器实例通过不同类加载器来完成隔离。非常巧妙。
- jsp类加载就是一个jsp对应一个加载器,修改之后删除并创建新的类加载器重新加载jsp的类文件。
为什么JDBC需要打破双亲委派机制
- 原因就是JDBC的DriverManager需要使用到Driver的实例,但是很明显这个实例并不是父类加载器能够加载的,他可以选择委托线程上下文类加载器进行加载,其实也就是应用类加载器去加载SPI。而使用线程上下文类加载器的原因就是需要使用到某个类对象,就可以通过线程进行传递带过去。
总结
最后做一个总结,写这篇文章的时候参考了很多博客,因为这部分只看书真的太难了,博客很多地方都带上源码,最直观的方式就是自己去把tomcat的部分源码拉到idea去看一看,对于jvm的类加载部分会有更深刻的理解。