一、类加载器分类

  • 引导类加载器由于是C/C++语言编写的,因此获取到的是null
  • 数组类的Class对象,不是由类加载器去创建的,而是在java运行期间JVM根据需要自动创建的,对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中的元素类型的类加载器是一样的;如果数组的元素类型是基本数据类型,数组类是没有类加载器的

    1. 引导类加载器

  • 这个加载器是由C/C++语言实现的,嵌套在JVM内部

  • 用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供Java自身需要的类
  • 并不继承ClassLoader,没有父加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、Javax、sun等开头的类·

    2. 扩展类加载器

  • java语言编写,由sun.misc.Launcher$ExtClassLoader实现

  • 间接继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

    3. 应用类加载器

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现

  • 间接继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统变量属性java.class.path 指定路径下的类库
  • 应用程序中的类默认使用系统类加载器加载
  • 它是用户自定义类加载器的默认父类加载器
  • 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

    4. 自定义类加载器

  • 通过类加载器可以实现非常绝妙的插件机制

  • 自定义加载器能够实现应用隔离
  • 自定义类加载器通常需要继承ClassLoader类

二、ClassLoader源码解析

类加载器的loadClass().png

1. 主要方法

loadClass()

  1. 加载名称为name的类,返回该类的实例。如果找不到类,则返回ClassNotFoundException
  2. 该方法中的逻辑实现就是双亲委派机制的实现

    findClass()

    实现类加载的主要逻辑代码,会调用defineClass()方法。一般实现自定义加载器要重写此方法

defineClass()

defineClass()通常与findClass()方法一起使用,defineClass() 生成类的Class对象

2. SecureClassLoader & URLClassLoader

SecureClassLoader
扩展了ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会跟它打交道,更多的是与URLClassLoader有所关联

URLClassLoader
实现了ClassLoader中的findClass()方法,新增了URLClassPath类协助取得Class字节码流等功能。如果没有过于复杂的需求,可以直接继承此类实现自定义类,这样代码更加简洁

3. Class.forname() & classLoader.loadClass()

Class.forname():
是一个静态方法,该方法在将Class文件加载到内存的同时,会执行类的初始化

classLoader.loadClass():
是一个实例方法,该方法将Class文件加载到内存时,并不会执行类的初始化

三、双亲委派模型

1. 优势与劣势

Ⅰ 优势

  • 避免类重复加载
  • 保护程序安全,防止核心API被随意修改

Ⅱ 劣势

由于双亲委派机制是单向的,上层的classLoader无法访问访问下层classLoader加载的类

2. 破坏双亲委派机制

⚪ 破坏双亲委派会不会影响核心API?

JDK为核心类库提供了一层保护机制,不管是什么类加载器,最终都会调用**defineClass()** 方法,而该方法会执行**preDefineClass()**方法,该方法提供了对JDK核心类库的保护

Ⅰ 第一次破坏

第一次“破坏”发生在双亲委派机制出现之前——即JDK1.2之前。双亲委派机制是在JDK1.2引入的,但是类加载器的概念和抽象类ClassLoader则在java的第一个版本就存在了,为了兼容这些已有的代码,无法再以技术的手段避免**loadClass()** 被子类覆盖的可能性。只能在JDK1.2之后新增findClass()方法

Ⅱ 第二次破坏

由于双亲委派机制的缺陷,引入了一个不太优雅的设计:线程上下文类加载器,这个类加载器同过java.lang.Thread类的**setContextClassLoader()** 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局作用范围都没有设置过的话,那这个类加载器默认就是应用程序类加载器

Ⅲ 第三次破坏

代码热替换和模块热部署的需求
IBM公司主导的JSR-291(即OSGi R4.2)实现模块化热部署的关键是它自定义的类加载机制的实现,每一个程序模块(OSGi 中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器的结构是一种网状结构

四、沙箱安全机制

  • 保护程序安全
  • 保护java原生的JDK代码

中篇_第4章:沙箱安全机制.jpg

五、自定义类加载器

1. 为什么要自定义类加载器

  1. 隔离加载类
  2. 修改类加载的方式
  3. 扩展加载源

比如从数据库、网络、甚至是电视机机顶盒进行加载

  1. 防止源码泄漏

进行编译加密,那么类夹加载器需要自定义,还原加密的字节码

2. 实现自定义加载器

Ⅰ 实现方式

  1. 继承classLoader类,重写loadClass()方法

破坏双亲委派机制

  1. 继承classLoader类,重写findClass()方法(建议用这个方式

编写好自定义的类加载器后,在程序中调用loadClass() 方法就可以实现类加载操作了


Ⅱ 代码实现

  1. public class MyClassLoader extends ClassLoader{
  2. // 字节码文件路径
  3. private String byteCodePath;
  4. public MyClassLoader(String byteCodePath) {
  5. this.byteCodePath = byteCodePath;
  6. }
  7. public MyClassLoader(ClassLoader parent, String byteCodePath) {
  8. super(parent);
  9. this.byteCodePath = byteCodePath;
  10. }
  11. @Override
  12. protected Class<?> findClass(String name) throws ClassNotFoundException {
  13. BufferedInputStream bis = null;
  14. ByteArrayOutputStream baos = null;
  15. try {
  16. // 获取字节码文件的完整路径
  17. String fileName = byteCodePath + name + ".class";
  18. bis = new BufferedInputStream(new FileInputStream(fileName));
  19. baos = new ByteArrayOutputStream();
  20. int len;
  21. byte[] data = new byte[1024];
  22. while ((len = bis.read(data)) != -1) {
  23. baos.write(data, 0, len);
  24. }
  25. // 获取内存中的完整的字节数组中的数据
  26. byte[] byteCodes = baos.toByteArray();
  27. // 调用defineClass(),将字节数组的数据转换为Class实例
  28. return defineClass(null, byteCodes, 0, byteCodes.length);
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. } finally {
  32. try {
  33. if (bis != null)
  34. bis.close();
  35. } catch (IOException e) {
  36. e.printStackTrace();
  37. }
  38. try {
  39. if (baos != null)
  40. baos.close();
  41. } catch (IOException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. return null;
  46. }
  47. }
  48. // 测试
  49. public class MyClassLoaderTest {
  50. public static void main(String[] args) {
  51. MyClassLoader loader = new MyClassLoader("d:/");
  52. try {
  53. Class clazz = loader.loadClass("Demo2");
  54. // 获取Demo2的类加载器
  55. System.out.println(clazz.getClassLoader().getClass().getName());
  56. System.out.println(clazz.getClassLoader().getParent().getClass().getName());
  57. } catch (ClassNotFoundException e) {
  58. e.printStackTrace();
  59. }
  60. }
  61. }

六、jdk9的新特性

在双亲委派之前,先判断该类是否能够归属到某个系统模块中,如果可以找到这样的归属关系,就要优先委派那个模块的类加载器完成加载
image.png