Java 类加载器classLoader | 七日打卡

628 阅读7分钟

类加载器是什么?

Java程序启动时(或者在运行的过程中), jvm(Java虚拟机)会完成类的加载, 其中第一步就是使用类加载器classLoader\color{red}{类加载器classLoader}将二进制流(.class文件)加载到内存中.

类加载器就是这样一个用来加载类文件的关键工具.

什么时候会触发类的加载?

Java虚拟机并没有强制要求何时必须完成对类的加载, 但是要求了类如果需要初始化的时候, 必须在这之前完成类的加载. (jvm在启动时肯定不会对所有类全部加载, 因为那样太占方法区的空间了, 因此类的加载也属于懒加载, 基本是在用的时候才加载)

因此可以认为类初始化是类加载的触发时间点\color{green}{类初始化是类加载的触发时间点}

Java虚拟机规定,有且只有以下五种\color{green}{五种}情况时,必须立即对类进行初始化:

  1. 虚拟机在用户指定包含main方法的主类后启动时

  2. 当使用 new 关键字对类进行实例化时、读取或者写入类的静态字段时、调用类的静态方法时,必须先触发对该类的实例化

  3. 使用反射对类进行反射调用时,如果该类没有初始化,必须先触发其初始化

  4. 初始化一个类,而该类父类还未初始化时,需要先对其父类进行初始化

  5. 在JDK7之后的版本中使用动态语言支持,java.lang.invoke.MethodHandle实例解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而该句柄对应的类还未初始化时,必须先触发其实例化

什么样的类是同一个类?

java 判断两个类是否是同一个类有两个条件

  1. 类的全名相同 (即包全路径 + 类名)

  2. 类加载器相同

类加载器有哪些?

如果只是常规使用, 我们无需关心类加载器的相关问题, jvm中内置了3个类加载器已经能满足我们的日常需要.

内置类加载器根据层级从上往下依次是启动类加载器 -> 扩展类加载器 -> 应用程序类加载器

如果你有需要自己加载类的需求, java支持通过自定义类加载器来实现

不同类加载器的作用范围

既然jvm中内置了三种不同的类加载器, 显然它们各司其职, 有所分工

中文英文作用范围(职责)
启动类加载器Bootstrap ClassLoader主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等
扩展类加载器Extention ClassLoader主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件
应用程序类加载器Application ClassLoader主要负责加载当前应用的classpath下的所有类
自定义类加载器User ClassLoader用户自定义的类加载器,可加载指定路径的class文件
可以看到, 不同的类加载器通过加载不同路径下的.class文件, 来完成各司其职的工作.

需要注意应用程序类加载器,就是靠它才会加载所有我们自己编写的类文件\color{red}{需要注意应用程序类加载器, 就是靠它才会加载所有我们自己编写的类文件}

类加载器如何协同工作?

限定加载范围就万事大吉了吗?

限定范围可以一定程度上保证不会对类进行重复加载, 但是无法保证下面两种情况

  1. 如果各个类加载器是平级的, 那么它们所加载的内容就失去了层次关系, 一旦不同路径下出现相同的内容, 就无法避免重复加载的问题

  2. 出于安全考虑, java本身的系统类, 是不能被随便加载的, 必须由指定的类加载器加载, 而不是让用户自行加载

双亲委派机制

为了解决上述两个问题, jvm除了限定不同类加载器的加载范围外, 通过双亲委派模式来运行.

所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。

限定范围的优势:

  1. 用户编写的类, 不会被启动类加载器和扩展类加载器去加载

  2. 核心api类 都是由启动类加载器或者扩展类加载器加载, 保证了安全

  3. 物理隔离范围, 不会主观去重复加载

双亲委派的优势:

  1. 可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。

  2. 保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。

为什么叫双亲?

用户自己的类默认都是通过应用程序类加载器加载的, 在应用程序类加载器加载之前, 会尝试通过启动类和扩展类加载器加载, 相当于两个父亲先加载, 所以叫双亲

类加载器是继承关系吗?

双亲委派只是表明一个层级和优先级的关系, 并不是继承关系

双亲委派机制的流程图

类加载器会使用loadClass方法来加载类(loadClass方法没有被重写的流程)

1、先检查类是否已经被加载过

2、若没有加载则调用父加载器的loadClass()方法进行加载

3、若父加载器为空则默认使用启动类加载器作为父加载器。

4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

核心方法分析

loadClass()

类加载的方法入口, 默认实现了双亲委派机制

findClass()

根据名称或位置加载.class字节码, 负责定位和找到具体的.class文件

defineClass()

把二进制流(字节码)转化为Class对象

自定义类加载器

java本身的三个类加载器能够满足日常使用, 但是jvm提供让用户自定义类加载器, 来控制类加载的第一步, 通过这种方式, 能非常灵活的实现一些需求

破坏双亲委派的自定义类加载器的例子

此类应用需要重写loadClass()方法

  1. 运行环境隔离.

tomcat:

Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。

不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。

如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。

Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反

  1. 热更新

jvm中类只能被加载一次, 如果想再次加载这个类, 只能通过一个新的类加载器来加载, 让每次实例化的对象都指向这个新的类。当这个类的 class 文件发生改变的时候,再次创建一个更新的类,之后如果系统再次发出实例化请求,创建的对象讲指向这个全新的类。

详情可以见热更新原理

不破坏双亲委派的自定义类加载器的例子

只需要重写findClass()方法

(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。

(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。