从零开始JVM实战高手,建议收藏,加源妹儿微信 “ ymsdsss”领取整套JVM实战资料和精品视频,关注微信公众号 “疯狂Java程序猿” ,后续会推出JVM,Mybatis,SpringBoot,Redis等等一些列从入门到源码剖析的精品视频和文章。你的鼓励是我最大的动力。
配套视频:https://www.bilibili.com/video/BV1nN411F7VT/?spm_id_from=333.337.search-card.all.click
作者:源码时代-Raymon老师
目前市面上已有太多的JVM相关教程和书籍,但是大部分偏理论,比较枯燥难懂,少有结合实际业务开发,站在项目开发的视角下去分析和讲解相关经验的教程;而本套教程会从零开始带着大家一步一步深入了解JVM底层原理,以及结合一些开发中的典型生产环境问题来进行实战剖析,并且几乎采用一步一图的方式进行讲解。
通过核心理论和实战案例的结合,希望能对大家对JVM的理解和应用更上一层楼。
类加载器的作用是用来把class文件加载到JVM内存中,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由 同一个类加载器加载 的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
下面是自定义类加载器的代码演示
/**
* 自定义的类加载器
* @author Raymon
*/
public class MyClassLoader extends ClassLoader{
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//获取到某个类的类名,比如cn.itsource.classloader.ClassLoaderTest,截取拼接后成为 ClassLoaderTest.class
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
//获取字节流
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
//如果没有找到该文件,交给上级加载器进行加载
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
下面是测试代码
/**
* 不同类加载加载同一个类的测试
* @author Raymon
*/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
//方式一:通过Class.forName的方式来获取到字节码对象;
Class<ClassLoaderTest> clazz1 = ClassLoaderTest.class;
//方式二:通过类名.class获取到字节码对象
Class<ClassLoaderTest> clazz2 = (Class<ClassLoaderTest>) Class.forName("cn.itsource.classloader.ClassLoaderTest");
//方式三:通过对象.getClass来获取到字节码对象;
ClassLoaderTest loaderTest = new ClassLoaderTest();
Class<ClassLoaderTest> clazz3 = (Class<ClassLoaderTest>) loaderTest.getClass();
System.out.println(clazz1 == clazz2);
System.out.println(clazz2 == clazz3);
//自定义类加载器加载cn.itsource.classloader.ClassLoaderTest这个类
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> clazz = myClassLoader.loadClass("cn.itsource.classloader.ClassLoaderTest");
System.out.println(clazz1 == clazz);
}
}
测试结果如下
Java的类加载器分为以下几种:
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader 【启动类加载器】 | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader 【扩展类加载器】 | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader【应用程序类加载器】 | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
Bootstrap ClassLoader主要是负责加载机器上安装的Java目录下的核心类文件,也就是JDK安装目录下jre目录下的lib目录,里面存放了一些Java运行所需要的核心的类库。当JVM启动的时候,会首先依托启动类加载器加载我们lib目录下的核心类库。
注意:如果我们定义了一个和jre/lib中相同的类(包名,类名,方法名都相同),我们的类是不会被加载的
Extension ClassLoader主要是加载jre目录下的lib目录下的ext中的文件,这里面的类用来支撑我们系统的运行。
Application ClassLoader该类加载器主要是加载“classpath”环境变量所指定的路径中的类,可以理解为加载我们自己写的Java代码 ,以及我的导入的三方Jar包中的代码。
注意:如果我们定义了一个和 三方Jar包中相同的类,会优先使用我们自己定义的。
除了以上三种以外,也可以自定义类加载器,根据具体的需求来加载对应的类。
注意:其实他们并没有继承关系
基于这个亲子层级机构,就有一个双亲委派机制,加载规则,优先级按照从上往下加载,如果上一级加载了下一级就不会再加载,如果上一级没有加载下一级就会进行加载,以此类推,到最后一级都没有加载成功,才报ClassNotFountException,这麽做的目的:
我们以应用程序类加载器举例,它在需要加载一个类的时候,不会直接去尝试加载,而是委托上级的扩展类加载器去加载,而扩展类加载器也是委托启动类加载器去加载.
启动类加载器在自己的搜索范围内没有找到这么一个类,表示自己无法加载,就再让扩展类加载器去加载,同样的,扩展类加载器在自己的。
搜索范围内找一遍,如果还是没有找到,就委托应用程序类加载器去加载.如果最终还是没找到,那就直接抛出异常了
这是为了安全着想,保证按照优先级加载.如果用户自己编写一个名为java.lang.Object的类,放到自己的Classpath中,没有这种优先级保证,应用程序类加载器就把这个当做Object加载到了内存中,从而会引发一片混乱.而凭借这种双亲委派机制,先一路向上委托,启动类加载器去找的时候,就把正确的Object加载到了内存中,后面再加载自行编写的Object的时候,是不会加载运行的.
结论:JDK自带的类是没法覆盖的,而引入的三方的JAR是可以自己定义相同的类来覆盖的。
下面是ClassLoader的源码
结合上一个案例我们可以Debug进行查看观察
虽然Ext类加载器的parent是null,但是当我们代码真正在执行的时候依然会去调用Bootstrap ClassLoader,因为Bootstrap ClassLoader并不是由Java代码实现的了,而是C++代码,所以这里的Ext类加载器的parent是null。
类加载器中的核心方法 loadClass 源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
使用之前的自定义类加载器加载First这个类,并Debug去观察程序的完整执行流程。
Class<?> clazz = classLoader.loadClass("cn.itsource.load.First");
首先我们来看下Tomcat类加载器的设计结构:
那么应用程序类加载器下的都是Tomcat自定义的类加载器,Tomcat为什么要自定义这么多类加载器又分别有什么用呢?我们通过以下图来进行说明:
首先Tomcat会通过Common类加载器来加载本地lib包下的核心文件,比如servlet-api.jar、jsp-api.jar、el-api.jar等,这些类可以供Tomcat以及所有的WebApp进行访问和使用
可以通过查看 conf/catalina.properties配置文件查看
common.loader=“ c a t a l i n a . b a s e / l i b " , " {catalina.base}/lib"," catalina.base/lib","{catalina.base}/lib/.jar"," c a t a l i n a . h o m e / l i b " , " {catalina.home}/lib"," catalina.home/lib","{catalina.home}/lib/.jar”
其次Catalina类加载器加载Tomcat应用程序所独有的一些类文件,这些文件对所有WebApp不可见,比如实现自己的会话存储方案。其路径由server.loader指定,默认为空,可以手动更改指定。
可以通过查看 conf/catalina.properties配置文件查看 server.loader= ,再次,Shared类加载器负责加载Web应用共享类,这些类tomcat服务器不会依赖。 ,可以通过查看 conf/catalina.properties配置文件查看, shared.loader=
而我们的WebApp类加载器主要是加载我们每个应用程序自己编写的代码,主要路径为: /WEB-INF/classes/目录下的Class和资源文件 以及 /WEB-INF/lib目录下的jar包,该类加载器加载的资源仅对当前应用程序可见,其他应用程序无法访问。并且WebApp类加载器可以使用到上级Shared类加载器和Common类加载器加载到的类。
最后JSP类加载器是为每一个JSP文件单独设计的一个类加载器,这也能解释为什么JSP文件被修改后不用重启服务器就能实现新的代码功能,这也是现在的热部署方案原因。当某一个JSP文件被修改后,对应的类加载器会被销毁重新创建新的一个JSP类加载器进行加载。
问题思考:
当我们的服务器中有多个应用程序的时候,并且都使用到了Spring来进行组织和管理,那么我们就可以把Spring放到Shared类加载器路径下进行加载,让所有应用程序进行共享,我们自己写的代码由于是WebApp加载器加载的所以访问上级Shared加载器加载的类是没问题的。但是Spring中的类要对应用程序中的类进行管理,如何访问呢?根据我们上文所说的双亲委派机制,显然是无法做到让上级类加载器去请求下级类加载器进行类加载的动作的。因此这里我们需要引出破坏性双亲委派机制。(如下图)
按主流的双亲委派机制,显然无法做到让父加载器加载的类去访问子类加载器加载的类,但使用线程上下文加载器可以让父类加载器请求子类加载器去完成类加载的动作。
因此spring根本不会去管自己被放在哪里,它统统使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean。
ContextLoaderListener监听器的作用就是启动Web容器时,自动装配ApplicationContext的配置信息,可以跟进源码查看
小结:
有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打破了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、 JDBC、JCE、JAXB和JBI等。
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由引导类加载器来加载的;SPI的实现类是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。
比如:在java中定义了接口
java.sql.Driver
,并没有具体的实现,具体的实现都是由不同厂商来提供的