众所周知, Java 或者其他运行在 JVM(java 虚拟机)上面的程序都需要最终便以为字节码,然后被 JVM加载运行,那么这个加载
到虚拟机的过程就是 classloader 类加载器所干的事情.直白一点,就是 通过一个类的全限定类名称来获取描述此类的二进制字节流 的过程.
说到 Java 的类加载器,必不可少的就是它的双亲委派模型,从 Java 虚拟机的角度来看,只存在两种不同的类加载器:
启动类加载器(Bootstrap ClassLoader), 由 C++语言实现,是虚拟机自身的一部分.
其他的类加载器,都是由 Java 实现,在虚拟机的外部,并且全部继承自java.lang.ClassLoader
在 Java 内部,绝大部分的程序都会使用 Java 内部提供的默认加载器.
启动类加载器(Bootstrap ClassLoader)
负责将$JAVA_HOME/lib
或者 -Xbootclasspath
参数指定路径下面的文件(按照文件名识别,如 rt.jar) 加载到虚拟机内存中.启动类加载器无法直接被 java 代码引用,如果需要把加载请求委派给启动类加载器,直接返回null
即可.
扩展类加载器(Extension ClassLoader)
负责加载$JAVA_HOME/lib/ext
目录中的文件,或者java.ext.dirs
系统变量所指定的路径的类库.
应用程序类加载器(Application ClassLoader)
一般是系统的默认加载器,比如用 main 方法启动就是用此类加载器,也就是说如果没有自定义过类加载器,同时它也是getSystemClassLoader()
的返回值.
这几种类加载器的工作流程被抽象成一个模型,就是双亲委派模型.
工作流程:
收到类加载的请求
首先不会自己尝试加载此类,而是委托给父类的加载器去完成.
如果父类加载器没有,继续寻找父类加载器.
搜索了一圈,发现都找不到,然后才是自己尝试加载此类.
这基本就是双亲委派模型.
但是这种模型只是一种推荐的方式,并不是强制的,你也可以尝试打破这种规则.
自所以这样约定,还是有一定的好处的, Java 类随着它的类加载器一起具备了一种带有优先级的层次关系.
比如自己定义了java.lang.Object
对象,那么按照上面的流程,他永远都是被启动类加载器加载的rt.jar 中的那个类,而不是自己定义的这个类,这样就保证了兄运行的稳定,否则,可能变得非常混乱,可以随意改写任何类.
大多数情况下,其实我们并不需要知道这些,因为你的程序也会运行的非常正常,虽然像Tomcat
,Spring Boot
都有自己定义的类加载器,但是我们在不用关心的情况下也会运行的好好地.
那么类加载器可以被运行在哪些地方呢?
从远程(或者文件)加载类,有时候需要加载的类可能并不是在当前的 classpath, 可能需要自己定义类加载器去加载.
自己想实现一个JavaAgent
来增强字节码的时候.
JavaAgent 的使用后续文章补上.先上一张图.
顶层是应用代码实际运行的 ClassLoader, 可能是Application ClassLoader
, 也有可能是 tomcat 的webapp ClassLoader
或者其他容器自定义的类加载器,总是是真实 的用户编写的代码运行的 classloader.
我们如果要在javaagent
中增强用户或者用户使用的包进行增强的话,必须实现一个自定义的 classloader 来"继承"(委派)应用代码的类加载器.为什么?
javaagent 的代码永远都是被应用类加载器( Application ClassLoader
)所加载,和应用代码的真实加载器无关,举个栗子,当前运行在 tomcat 中的代码是webapp ClassLoader
加载的,如果启动参数加上-javaagent
, 这个 javaagent 还是在Application ClassLoader
中加载的.
按照上面的双亲委派模型,如果我们在 javaagent 中想要访问应用里面的 api 包或者类,这是不可能的,因为按照双亲委派模型,通俗来说就是,子加载器可以访问父加载器中的类,但是反过来就行不通.
那么这个时候有没有办法能够做到呢?
我们可以自定义自己的类加载器继承应用代码类加载器(可以在 javaagent 中完成, javaagent 每加载一个类,就会回调传回真实的类加载器),然后我们在Application ClassLoader
中用自定义的类加载器去加载子类,并创建好实例(newInstance()
), 将实例的引用保存 在变量中.
真实运行的时候,就会通过这个变量,去访问我们自定义加载器的内容,又由于我们的自定义类加载器是继承自应用代码的类加载器的,所以自定义类加载器中的代码可以访问应用的代码.
总结一句就是,父类加载器无法加载子类加载器的类,但是可以持有子类加载器所加载类的实例,从而实现父类加载器的代码可以调用子类加载器的代码的形式
貌似比较抽象,后面会补上详细的例子供参考.
例子
针对上面的情形,我们定义一个例子,可以详细解释 ClassLoader 的加载使用,
假如我们有如下的 ClassLoader,FooClassLoader
:
被加载的类定义,然后我们将这个类放到不是源代码的路径比如我放到/Users/lican/git/test/foo/
这里的,主要是方便测试.
然后测试程序为:
我们用FooClassLoader
来加载com.example.test.FooTest
, 然后在 AppClassLoader中持有引用.被后续使用.
PS:关注360linker公众号,加入官方社区获取免费视频教程、知名单位招聘信息。交流分享IT圈学习经验。