JVM系列(十九):类的加载器

类的加载器详解

  • 类加载器时JVM执行类加载机制的前提
  • ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载
  • ClassLoader通过各种方式将二进制数据流读入JVM内部,转为为一个java.lang.Class对象实例
  • ClassLoader在整个装载阶段中,只能影响到类的加载,链接和初始化由JVM负责。
  • 代码是否能运行,由执行引擎决定


1、概述

1.1、相关面试题

1.2、类的加载分类(不是加载器分类)

  • 显式加载vs隐式加载
  • (加载的Class对象是每个类对应的唯一实例java.lang.Class)

1.2.1、显式加载

  • 调用ClassLoader来加载class对象,如:
  1. Class.forName()
  2. This.getClass().getClassLoader().loadClass()

1.2.2、隐式加载

  • 隐式加载:虚拟机自动加载Class对象到内存中,比如new一个对象时

1.3、类的加载器的必要性

  • 避免开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常,遇到了可以定位问题
  • 在支持类的动态加载、编译后字节码文件加解密时需要
  • 可以自定义类的加载器来重新定义类的加载规则

1.4、命名空间

  • 类的唯一性
  1. 由该类本身和类加载器(类加载器实例,同一个加载器类的不同实例也算不同命名空间)一起确认类在JVM中的唯一性
  2. 比较类是否想等,除了类本身,还要看加载器,如果加载器不一样,比如使用自定义加载器,那么类不一样
  • 命名空间
  1. 每个类加载器都有自己的命名空间,命名空间由该加载器(加载器实例)和所有的父加载器所加载的组成的
  2. 同一命名空间中,不会出现类名字一样的两个类
  3. 不同命名空间中,可能出现类名字一样的两个类(因为加载器不同)
  4. 利用该性质可以在大型应用中运行一个类的不同版本

1.5、类加载机制的基本特征

  • 双亲委派机制,并不是所有类都遵守
  • 可见性,子类加载器可以访问父类加载器加载的类型,反过来是不可以的,即父类加载器不可以访问子类加载器加载的类型
  • 单一性,父类加载器加载的类型对于子类时可见的,所以父类加载器加载过的类型,子类加载器不会重复加载了。但是“邻居”,即同一级别的加载器加载的类型可以加载多次,互不可见(包含自定义的同一个加载器类不同的实例对象)

2、类的加载器分类

  • 看似继承关系,实际是包含关系,下层加载器中包含着上层加载器的引用

2.1、​​​​​​​引导类加载器(启动类加载器)

  • 由C/C++语言实现,嵌套在JVM内部
  • 加载Java的核心库,提供JVM自身需要的类
  1. JAVA_HOME/jre/lib/rt.jar
  2. sun.boot.class.path
  • 不需要继承java.lang.ClassLoader,没有父加载器
  • 只加载包名为java、javax、sun开头的类
  • 拓展类和系统类加载器,也是由启动类加载器来加载,并且指定为他们的父类加载器(就是要使用拓展类和系统类加载器的时候,由启动类加载器来将其加载进入JVM)

2.2、拓展类加载器

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 继承于ClassLoader类(间接继承)
  • 父类加载器为启动类加载器
  • 加载目录:
  1. 系统属性指定的java.ext.dirs
  2. JDK安装目录的jre/lib/ext
  3. 如果用户创建的jar包放在以上目录,也由拓展类加载器加载

​​​​​​​2.3、系统类加载器

  • Java语言编写,由sun.misc.Launch$AppClassLoader实现
  • 继承于ClassLoader(间接加载)
  • 父类加载器为拓展类加载器
  • 负责加载:
  1. 环境变量classpath
  2. 系统属性java.class.path指定路径下的类库
  • 默认为应用程序中的类加载器
  • 默认为用户自定义类加载器的父加载器
  • 可以通过ClassLoader的getSystemClassLoader()方法获取

2.4、用户自定义类加载器

  • 可以自定义类加载器来加载本地jar包或网络远程资源
  • 通过自定义类加载器,可以实现插件机制,可以为应用程序提供一种动态新增功能的机制,无须重新打包发布应用程序(?How)
  • 通过自定义类加载器,可以实现应用隔离,隔离不同的组件模块
  • 需要继承ClassLoader

3、测试不同的类加载器

  • 获取ClassLoader的途径

  • 加载数组类,使用的类加载器由数组元素决定,使用元素的类加载器
  1. String使用引导类加载器
  2. 基本数据类型不需要引导类加载器
  3. 用户自定义类使用系统类加载器

4、ClassLoader源码解析

  • ClassLoader源码关系

4.1、ClassLoader的方法

  • Public final ClassLoader getParent(),返回该类加载器的超类加载器
  • Public Class<?> loadClass(String name) throws ClassNotFoundException,加载名称为name的类,返回其java.lang.Class实例,在其内部实现双亲委派机制
  • Protected Class<?> findClass(String name) throws ClassNotFoundException,查找二进制中名称为name的类,返回java.lang.Class类实例。在URLClassLoader类重写。JVM鼓励自定义加载器时重写findClass,而不是重写loadClass,因为loadClass中使用双亲委派机制,鼓励使用双亲委派。Protected只有在自定义的ClassLoader的子类中能使用
  • Protected final Class<?> defineClass(String name, byte[] b, int off, int len),根据给定的字节数组b转化为Class实例,off和len是实际的Class信息在byte数组中的位置和长度,Protected只有在自定义的ClassLoader的子类中能使用

4.2、​​​​​​​SecureClassLoaderURLClassLoader

  • SecureClassLoader,拓展了ClassLoader,新增一些验证功能,一般不改这个类
  • URLClassLoader,具体实现了findClass()和findResource()等方法。在自定义类的加载器的时候,如果不需要太多复杂的功能,可以继承URLClassLoader,避免自己编写findClass()方法,直接使用URLClassLoader. findClass()去获取字节码流,使自定义类加载器编写更加简洁

4.3、​​​​​​​ExtClassLoaderAppClassLoader

  • ExtClassLoader没有重写loadClass()方法,所以遵循双亲委派机制
  • AppClassLoader重载了loadClass()方法,但是最终还是调用父类的loadClass()方法,所以也遵守双亲委派机制

4.4、​​​​​​​Class.forName()ClassLoader.loadClass()----面试问

  • Class.forName()
  1. 是一个静态方法
  2. 根据传入的类的全限定名返回Class对象
  3. 属于主动使用,在类文件加载到内存的同时,执行类的初始化
  • ClassLoader.loadClass
  1. 实例方法,需要一个ClassLoader对象来调用
  2. 该方法属于被动使用,将类文件加载到内存时,不会执行类的初始化,一直到这个类真正使用的时候才会进行初始化

5、双亲委派机制

  • 从JDK1.2开始,类的加载采用双亲委派机制,保证Java平台的安全

5.1、​​​​​​​定义与本质

  • 定义:类加载器接收到加载请求时,首先请求父类加载器完成加载,依次递归请求,如果父类加载器能加载,就成功返回;如果不能,则自己加载

  • 本质:限定加载顺序:引导类加载器à拓展类加载器à系统类加载器或用户自定义加载器

5.2、​​​​​​​优劣势

  • 优势
  1. 避免类的重复加载,确保类的全局唯一性
  2. 保护程序安全,防止核心API被篡改,比如自定义java.lang包,并在包下创建String类,如果没有双亲委派机制,直接使用系统类加载器加载,如果自定义的String有恶意代码,就糟糕了
  • 代码支持:双亲委派机制在java.lang.ClassLoader.loadClass(String, boolean)接口中体现(直接有具体实现方法),逻辑如下:
  1. 在当前加载器的缓存中查找有无目标类,如果有,返回
  2. 判断当前加载器的父加载器是否为空,如果不为空,调用parent.loadClass(name, false)进行加载;如果为空,那么执行下一步
  3. 当前加载器的父类加载器为空,那么调用finaBootstrapClassOrNull(name),让引导类加载器进行加载
  4. 以上三个步骤都没有能加载,那么调用findClass(name)进行加载,该方法/接口最终调用java.lang.ClassLoader中的defineClass系列方法的native接口加载目标Java类
  • 重写loadClass(String, boolean)方法,将其中的双亲委派机制代码去掉,那么是否会直接导致安全问题呢?不会,因为:JDK核心类库提供一层保护机制,无论是自定义的类加载器还是系统类加载器、拓展类加载器,最终都必须调用java.lang.ClassLoader.defineClass(String, byte[], int, int, ProtectionDomain)方法,该方法会执行preDefineClass()接口,该接口中提供了堆JDK核心类库的保护
  • 双亲委派机制弊端
  1. 底层可以访问上层加载器加载的类,但是上层不能访问底层加载器加载的类
  2. Java虚拟机规范中建议使用双亲委派机制,并不是一定使用

5.3、​​​​​​​破坏双亲委派机制

5.3.1、第一次破坏

  • 类的加载器和抽象类java.lang.ClassLoader在第一版JDK中就出现了,但是双亲委派机制是JDK1.2出现的。
  • 为了兼容已经存在的用户自定义的类加载器的代码,在java.lang.ClassLoader中定义新的protected方法findClass(),引导用户重写findClass()方法而不是loadClass()方法,因为双亲委派机制的逻辑在loadClass方法中实现,所以其逻辑能得到保留
  • 如果在loadClass方法中,父类加载器加载失败了,那么调用自己加载器的findClass()方法来加载类

5.3.2、第二次破坏

  • 底层的类加载器可以访问上层的类加载器加载的类型,但是上层的类加载器无法访问底层的类加载器加载的类型。
  • 当上层类加载器加载的类型中需要调用底层类加载的类型时,会出现无法访问的问题
  • 为了解决这个问题,引入一个不太优雅的设计:线程上下文类加载器,通过这个类加载器去加载所需要的代码
  1. 可以通过java.lang.Thread类的setContextClassLoader方法来设置加载器
  2. 如果线程创建时还未设置加载器,那么从父线程中继承
  3. 如果整个全局中都没设置,那么就默认系统类加载器为上下文类加载器(就是默认使用最底层的类加载器),其实这也就破坏了双亲委派机制
  • 为了更好的实现上层类加载器加载类型访问底层,JDK6提供了java.unti.ServiceLoader类,使用META-INF/services中的配置信息,辅以责任链模式,提供一种相对合理的解决方案

5.3.3、第三次破坏

  • 第三次破坏是由用户对程序动态性的追求导致的
  • 就是希望Java代码像电脑鼠标一样,即插即用
  • OSGi中的解决:每个程序模块(称为Bundle)都有自己的类加载器,类加载器不再是树状模型,而是拓展为网状模型
  1. 以java.*开头的,委托给父类加载器加载
  2. 否则,将委派列表名单中的类,委托给父类加载器加载
  3. 否则,将Import列表中的类,委托给Export这个类的Bundle的类加载器加载
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委托给Fragment Bundle的类加载器加载
  6. 否则,查找Dynamic Import列表的Bundle,将其委派给对应的Bundle的类加载器加载
  7. 否则,类的查找失败
  8. 第一和第二点还是符合双亲委派模型的,后面的就不符合了,就去查找平级的类加载器加载了。

5.4、​​​​​​​热替换实现

  • 热替换就是在程序运行过程中,不停止服务,仅替换程序文件来修改文件。大部分脚本语言天生支持热替换(如PHP),但是Java不支持。
  • 在Java中,如果一个类已经加载到系统中,通过修改类文件,无法使系统重新加载这个类。即使使用的不同的ClassLoader加载该类,那么在JVM内存中也是属于不同的类型,不能相互兼容(因为两个类加载器加载的同一个类也是不同的)
  • 热替换思路:(直接实现方法级别的替换):

  • 一直在循环,每次循环中都去创建自定义的ClassLoader实例,然后利用自定义的ClassLoader实例去加载类文件,然后创建类的实例,直接调用新实例的方法

6、沙箱安全机制

  • 沙箱就是限制程序运行时的环境
  • Java的沙箱安全机制,将Java代码限定在JVM的特定运行范围中,并且严格限制代码对本地系统资源的方法,通过隔离措施保证安全
  • 限制的资源有:CPU、内存、文件系统、网络等,不同级别的沙箱对资源访问的限制不同
  • Java安全模型的核心就是Java沙箱(sandbox)

6.1、​​​​​​​JDK1.0时期

  • Java程序分为本地代码和远程代码,本地代码默认可信,远程代码不可信
  • 本地代码可以访问一切本地资源
  • 远程代码在沙箱中运行

​​​​​​​6.2、JDK1.1时期

  • 增加安全策略,允许可信任的远程代码访问本地资源

6.3、​​​​​​​JDK1.2时期

  • 无论什么代码,都按照相应的权限组来访问资源

6.4、​​​​​​JDK1.6时期

  • 目前最新的安全机制,引入域的概念,分为应用域和系统域
  • 应用域中分为各个小域(其实就是JDK1.2时期的权限组),通过系统域的部分代理来对各种资源进行访问
  • 系统域专门负责与关键信息进行交互


7、用户自定义类加载器

7.1、为什么自定义类加载器

  • 隔离加载类,通过自定义类加载器,确保将类加载到不同的环境中
  • 修改类加载的方式,按照实际需求,不一定使用JVM默认的加载机制(双亲委派)
  • 拓展加载源,可以从数据库、网络、机顶盒加载二进制文件
  • 防止源码泄露,Java代码容易被编译和篡改,所以可以在编译时加密,然后自定义类加载器来还原加密的字节码

7.2、类型转换要求

  • 一般情况,使用不同的类加载器加载不同的功能模块,会提高安全性。但是如果涉及到Java类型转换,会出问题。在类型转换时,两种类型必须由同一个加载器加载,才能进行类型转换,否则会发生异常

7.3、实现自定义类加载器

  • 实现方式,一般继承java.lang.ClassLoader,当然也可以继承其他,选择重写两种方法:
  1. 重写loadClass()
  2. 重写findClass(),推荐
  • 在loadClass中,在实现双亲委派机制逻辑后,会调用findClass,所以一般不重写loadClass,自定义好以后,可以在程序中调用loadClass即可(Java的核心类库也是使用loadClass方法)
  • 自定义类加载器的父类加载器是系统类加载器
  • 自定义类的加载器,一定要把加载的class文件存放到别的目录下面,否则会自动使用应用类加载器加载(要加载的类要放到别的目录下)

8、JDK9中新特性

  • 拓展机制被移出了,拓展类加载器由于向后兼容被保留,重新命名为平台类加载器器(platform class loader),可以通过ClassLoader的新方法getPlatformClassLoader获取
  1. JDK9进行了模块话构建,将原来的rt.jar和tools.jar拆成了数十个JMOD文件
  2. 无需保留JAVA_HOME\lib\ext目录,此前使用这个目录或者java.ext.dirs系统变量来拓展JDK功能的机制没有存在的价值了
  • 平台类加载器和应用程序类加载器不再继承自java.net.URLClassLoader,启动类加载器、平台类加载器、应用程序类加载器都继承自jdk.internal.loader.BuiltinClassLoader

  • JDK9中,类加载器有了新的名称,平台类加载器名为platform,应用类加载器名为app
  • 启动类加载器不再全部有C/C++实现,而是有jvm内部和Java类库共同协作实现,为了与先前版本兼容,在获取启动类加载器时依旧返回null,不会得到BootClassLoader实例
  • 双亲委派机制的改变

       (当平台类和应用程序类加载器收到类加载请求,再委派给父类加载器之前,要先判断该类是否归属到某一系统模块中,如果可以找到这样的归属关系,那么优先委派给这个模块的加载器完成加载)​​​​​​​

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值