转载自   深入理解Java ClassLoader及在 JavaAgent 中的应用

背景

众所周知, Java 或者其他运行在 JVM(java 虚拟机)上面的程序都需要最终便以为字节码,然后被 JVM加载运行,那么这个加载到虚拟机的过程就是 classloader 类加载器所干的事情.直白一点,就是 通过一个类的全限定类名称来获取描述此类的二进制字节流 的过程.

双亲委派模型

说到 Java 的类加载器,必不可少的就是它的双亲委派模型,从 Java 虚拟机的角度来看,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader), 由 C++语言实现,是虚拟机自身的一部分.
  2. 其他的类加载器,都是由 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() 的返回值.

这几种类加载器的工作流程被抽象成一个模型,就是双亲委派模型.

工作流程:

  1. 收到类加载的请求
  2. 首先不会自己尝试加载此类,而是委托给父类的加载器去完成.
  3. 如果父类加载器没有,继续寻找父类加载器.
  4. 搜索了一圈,发现都找不到,然后才是自己尝试加载此类.

这基本就是双亲委派模型.

但是这种模型只是一种推荐的方式,并不是强制的,你也可以尝试打破这种规则.
自所以这样约定,还是有一定的好处的, Java 类随着它的类加载器一起具备了一种带有优先级的层次关系.
比如自己定义了java.lang.Object 对象,那么按照上面的流程,他永远都是被启动类加载器加载的rt.jar 中的那个类,而不是自己定义的这个类,这样就保证了兄运行的稳定,否则,可能变得非常混乱,可以随意改写任何类.

在 JavaAgent 中的应用

大多数情况下,其实我们并不需要知道这些,因为你的程序也会运行的非常正常,虽然像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 的加载使用,

  1. 假如我们有如下的 ClassLoader,FooClassLoader:
package com.example.test;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;/*** @author lican*/
public class FooClassLoader extends ClassLoader {private static final String NAME = "/Users/lican/git/test/foo/";@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Class<?> loadedClass = findLoadedClass(name);if (loadedClass == null) {String s = name.substring(name.lastIndexOf(".") + 1) + ".class";File file = new File(NAME + s);try (FileInputStream fileInputStream = new FileInputStream(file)) {byte[] b = new byte[fileInputStream.available()];fileInputStream.read(b);return defineClass(name, b, 0, b.length);} catch (IOException e) {e.printStackTrace();}}return loadedClass;}}
  1. 被加载的类定义,然后我们将这个类放到不是源代码的路径比如我放到
    /Users/lican/git/test/foo/这里的,主要是方便测试.

package com.example.test;public class FooTest {public String getFoo() {return "foo";}
}

然后测试程序为:

package com.example.test;import java.lang.reflect.Method;/*** @author lican*/
public class ClassLoaderTest {private Object fooTestInstance;private FooClassLoader fooClassLoader = new FooClassLoader();public static void main(String[] args) throws Exception {ClassLoaderTest classLoaderTest = new ClassLoaderTest();classLoaderTest.initAndLoad();Object fooTestInstance = classLoaderTest.getFooTestInstance();System.out.println(fooTestInstance.getClass().getClassLoader());Method getFoo = fooTestInstance.getClass().getMethod("getFoo");System.out.println(getFoo.invoke(fooTestInstance));System.out.println(classLoaderTest.getClass().getClassLoader());}private void initAndLoad() throws Exception {Class<?> aClass = Class.forName("com.example.test.FooTest", true, fooClassLoader);fooTestInstance = aClass.newInstance();}public Object getFooTestInstance() {return fooTestInstance;}
}

我们用FooClassLoader来加载com.example.test.FooTest, 然后在 AppClassLoader中持有引用.被后续使用.

引用

  • 深入理解 Java 虚拟机(第二版)

深入理解Java ClassLoader及在 JavaAgent 中的应用相关推荐

  1. 理解Java ClassLoader机制 |用Java说话,人气战胜时间!Come On

    理解Java ClassLoader机制 |用Java说话,人气战胜时间!Come On 我在参加一个比赛. 欢迎大家都来我的网站参观一下. http://home.fego.cn/members/l ...

  2. java stw_快速理解Java垃圾回收和jvm中的stw

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外).Java中一种全局暂停现象,全局停顿,所有Java代码停 ...

  3. 【java】深入理解Java JVM虚拟机中init和clinit的区别

    1.概述 转载并且补充:深入理解Java JVM虚拟机中init和clinit的区别 原创 之所以来转载一下,是因为,有个学弟问了这个问题 这个知识点,回顾起来应该是我看 深入理解Java JVM虚拟 ...

  4. 了解和扩展Java ClassLoader

    Java ClassLoader是项目开发中Java的关键但很少使用的组件之一. 就我个人而言,我从未在任何项目中扩展ClassLoader,但是拥有自己的可以自定义Java类加载的ClassLoad ...

  5. java事件处理模型_从零开始理解JAVA事件处理机制(3)

    我们连续写了两小节的教师-学生的例子,必然觉得无聊死了,这样的例子我们就是玩上100遍,还是不知道该怎么写真实的代码.那从本节开始,我们开始往真实代码上面去靠拢. 事件最容易理解的例子是鼠标事件:我们 ...

  6. 深入理解Java虚拟机:jvm内存模型jdk1.8

    深入理解Java虚拟机:jvm内存模型jdk1.8 一.程序计数器 使用PC寄存器存储字节码指令地址有什么作用?为什么使PC寄存器记录当前线程的执行地址? PC寄存器为什么会被设定为线程私有? 二.J ...

  7. 深入理解Java虚拟机垃圾回收机制

    文章目录 什么是垃圾回收 哪些内存需要被回收?什么时候回收?如何回收? 哪些内存需要被回收?什么时候回收? 引用计数算法 可达性分析算法 如何回收?(垃圾收集算法) 标记-清除算法 复制算法 标记-整 ...

  8. 老大难的 Java ClassLoader,到了该彻底理解它的时候了

    ClassLoader 是 Java 届最为神秘的技术之一,无数人被它伤透了脑筋,摸不清门道究竟在哪里.网上的文章也是一篇又一篇,经过本人的亲自鉴定,绝大部分内容都是在误导别人.本文我带读者彻底吃透 ...

  9. 运用《深入理解Java虚拟机》书中知识解决实际问题

    前言 以前看别人博客说看完<深入理解Java虚拟机>这本书并没有让自己的编程水平提高多少,不过却大大提高了自己的装逼水平.其实,我倒不这么认为,至少在我看完一遍这本书后,有一种醍醐灌顶的感 ...

最新文章

  1. SpringMVC接收checkbox传值
  2. SCIP习题 1.21(寻找最小因子)
  3. C#——继承[模拟Server类]初始化过程顺序DMEO
  4. Win7+Ubuntu双系统结构下,Ubuntu克隆至新硬盘,启动成功
  5. 基于JAVA+SpringBoot+Mybatis+MYSQL的化妆品售卖系统
  6. Linux自动注销登录的帐户
  7. 离线安装老版本android sdk,亲测,linux、windows、mac通用
  8. WinEdt LaTex(二)—— 空心中括号
  9. 汇编语言 王爽 第四版 实验4
  10. 数字日期格式转换yyyymmdd_js时间转换,能够把时间转换成yyyymmdd格式或yyyymm格式...
  11. 如何用ps做一个魔法棒
  12. android edittext删除文本框,Android EditText 文本框实现搜索和清空效果
  13. 七夕节来用python表白吧!爱情病毒浸染你的心!
  14. 网站劫持 网站(域名)被劫持怎么检测 遇到网站恶意跳转不要慌(干货)
  15. 从游戏中学习产品设计2:消费篇
  16. 2022年起重机司机(限桥式起重机)考试题库及答案
  17. html websocket
  18. mybatis oracle两种方式批量插入数据(带序号)
  19. 弃玩《列王的纷争》之感
  20. 工作心路历程系列2:离职大公司入职创业公司心路历程

热门文章

  1. 杂牌手柄模拟xboxone手柄_手机就能玩Switch游戏,蛋蛋模拟器+盖世小鸡X2手柄体验...
  2. [Java基础]接口组成(默认方法,静态方法,私有方法)
  3. sqrt()函数的注意事项
  4. JVM(3)——JVM类加载器
  5. 按照前序遍历和中序遍历构建二叉树
  6. 伯努利数(详解 + 例题 :P3711 仓鼠的数学题)
  7. 牛客练习赛69 解方程
  8. Technocup 2020 - Elimination Round 2 E. Rock Is Push dp
  9. CF1253E Antenna Coverage
  10. 牛客题霸 [数组中出现次数超过一半的数字] C++题解/答案