一, 类加载的机制的层次结构
每个编写的 ".java" 拓展名类文件都存储着需要执行的程序逻辑, 这些 ".java" 文件经过 Java 编译器编译成拓展名为 ".class" 的文件,".class" 文件中保存着 Java 代码经转换后的虚拟机指令, 当需要使用某个类时, 虚拟机将会加载它的 ".class" 文件, 并创建对应的 class 对象, 将 class 文件加载到虚拟机的内存, 这个过程称为类加载, 这里我们需要了解一下类加载的过程, 如下:
Jvm 执行 class 文件
步骤一, 类加载机制
将 class 文件字节码内容加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构, 在堆中生成一个代表这个类的 java.lang.Class 对象, 作为方法区类数据的访问入口, 这个过程需要类加载器参与.
当系统运行时, 类加载器将. class 文件的二进制数据从外部存储器 (如光盘, 硬盘) 调入内存中, CPU 再从内存中读取指令和数据进行运算, 并将运算结果存入内存中. 内存在该过程中充当着 "二传手" 的作用, 通俗的讲, 如果没有内存, 类加载器从外部存储设备调入. class 文件二进制数据直接给 CPU 处理, 而由于 CPU 的处理速度远远大于调入数据的速度, 容易造成数据的脱节, 所以需要内存起缓冲作用.
类将. class 文件加载至运行时的方法区后, 会在堆中创建一个 Java.lang.Class 对象, 用来封装类位于方法区内的数据结构, 该 Class 对象是在加载类的过程中创建的, 每个类都对应有一个 Class 类型的对象, Class 类的构造方法是私有的, 只有 JVM 能够创建. 因此 Class 对象是反射的入口, 使用该对象就可以获得目标类所关联的. class 文件中具体的数据结构.
类加载的最终产物就是位于堆中的 Class 对象(注意不是目标类对象), 该对象封装了类在方法区中的数据结构, 并且向用户提供了访问方法区数据结构的接口, 即 Java 反射的接口.
步骤二, 连接过程
将 java 类的二进制代码合并到 JVM 的运行状态之中的过程
验证: 确保加载的类信息符合 JVM 规范, 没有安全方面的问题
准备: 正式为类变量 (static 变量) 分配内存并设置类变量初始值的阶段, 这些内存都将在方法区中进行分配
解析: 虚拟机常量池的符号引用替换为字节引用过程
步骤三, 初始化
初始化阶段是执行类构造器 < clinit>()方法的过程. 类构造器 < clinit>()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块 (static 块) 中的语句合并产生, 代码从上往下执行.
当初始化一个类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化
虚拟机会保证一个类的 < clinit>()方法在多线程环境中被正确加锁和同步 当范围一个 Java 类的静态域时, 只有真正声名这个域的类才会被初始化
二, 类加载器的层次结构
启动 (Bootstrap) 类加载器
扩展 (Extension) 类加载器
系统 (-) 类加载器
1, 启动 (Bootstrap) 类加载器
启动类加载器主要加载的是 JVM 自身需要的类, 这个类加载使用 C++ 语言实现的, 是虚拟机自身的一部分, 它负责将 < JAVA_HOME>/lib 路径下的核心类库或 - Xbootclasspath 参数指定的路径下的 jar 包加载到内存中, 注意必由于虚拟机是按照文件名识别加载 jar 包的, 如 rt.jar, 如果文件名不被虚拟机识别, 即使把 jar 包丢到 lib 目录下也是没有作用的(出于安全考虑, Bootstrap 启动类加载器只加载包名为 java,javax,sun 等开头的类).
2, 扩展 (Extension) 类加载器
扩展类加载器是指 Sun 公司 (已被 Oracle 收购) 实现的 sun.misc.Launcher$ExtClassLoader 类, 由 Java 语言实现的, 是 Launcher 的静态内部类, 它负责加载 < JAVA_HOME>/lib/ext 目录下或者由系统变量 - Djava.ext.dir 指定位路径中的类库, 开发者可以直接使用标准扩展类加载器.
3, 系统 (System) 类加载器
也称应用程序加载器是指 Sun 公司实现的 sun.misc.Launcher$AppClassLoader. 它负责加载系统类路径 java -classpath 或 - D java.class.path 指定路径下的类库, 也就是我们经常用到的 classpath 路径, 开发者可以直接使用系统类加载器, 一般情况下该类加载是程序中默认的类加载器, 通过 ClassLoader#getSystemClassLoader()方法可以获取到该类加载器.
在 Java 的日常应用程序开发中, 类的加载几乎是由上述 3 种类加载器相互配合执行的, 在必要时, 我们还可以自定义类加载器, 需要注意的是, Java 虚拟机对 class 文件采用的是按需加载的方式, 也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象, 而且加载某个类的 class 文件时, Java 虚拟机采用的是双亲委派模式即把请求交由父类处理, 它一种任务委派模式, 下面我们进一步了解它.
3.1, 理解双亲委派模式
下面我们从代码层面了解几个 Java 中定义的类加载器及其双亲委派模式的实现, 它们类图关系如下
双亲委派模式是在 Java 1.2 后引入的, 其工作原理的是, 如果一个类加载器收到了类加载请求, 它并不会自己先去加载, 而是把这个请求委托给父类的加载器去执行, 如果父类加载器还存在其父类加载器, 则进一步向上委托, 依次递归, 请求最终将到达顶层的启动类加载器, 如果父类加载器可以完成类加载任务, 就成功返回, 倘若父类加载器无法完成此加载任务, 子加载器才会尝试自己去加载, 这就是双亲委派模式, 即每个儿子都很懒, 每次有活就丢给父亲去干, 直到父亲说这件事我也干不了时, 儿子自己想办法去完成, 这不就是传说中的实力坑爹啊? 那么采用这种模式有啥用呢?
3.1, 双亲委派模式优势
采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系, 通过这种层级关可以避免类的重复加载, 当父亲已经加载了该类时, 就没有必要子 ClassLoader 再加载一次. 其次是考虑到安全因素, java 核心 API 中定义类型不会被随意替换, 假设通过网络传递一个名为 java.lang.Integer 的类, 通过双亲委托模式传递到启动类加载器, 而启动类加载器在核心 Java API 发现这个名字的类, 发现该类已被加载, 并不会重新加载网络传递的过来的 java.lang.Integer, 而直接返回已加载过的 Integer.class, 这样便可以防止核心 API 库被随意篡改. 可能你会想, 如果我们在 classpath 路径下自定义一个名为 java.lang.SingleInterge 类 (该类是胡编的) 呢? 该类并不存在 java.lang 中, 经过双亲委托模式, 传递到启动类加载器中, 由于父类加载器路径下并没有该类, 所以不会加载, 将反向委托给子类加载器加载, 最终会通过系统类加载器加载该类. 但是这样做是不允许, 因为 java.lang 是核心 API 包, 需要访问权限, 强制加载将会报出如下异常
java.lang.SecurityException: Prohibited package name: java.lang
所以无论如何都无法加载成功的.
三, 类加载器间的关系
我们进一步了解类加载器间的关系(并非指继承关系), 主要可以分为以下 4 点
启动类加载器, 由 C++ 实现, 没有父类.
拓展类加载器(ExtClassLoader), 由 Java 语言实现, 父类加载器为 null
系统类加载器(AppClassLoader), 由 Java 语言实现, 父类加载器为 ExtClassLoader
自定义类加载器, 父类加载器肯定为 AppClassLoader.
1, 类加载器常用方法
loadClass(String)
该方法加载指定名称 (包括包名) 的二进制类型, 该方法在 JDK1.2 之后不再建议用户重写但用户可以直接调用该方法, loadClass()方法是 ClassLoader 类自己实现的, 该方法中的逻辑就是双亲委派模式的实现, 其源码如下, loadClass(String name, boolean resolve)是一个重载方法, resolve 参数代表是否生成 class 对象的同时进行解析相关操作.
正如 loadClass 方法所展示的, 当类加载请求到来时, 先从缓存中查找该类对象, 如果存在直接返回, 如果不存在则交给该类加载去的父加载器去加载, 倘若没有父加载则交给顶级启动类加载器去加载, 最后倘若仍没有找到, 则使用 findClass()方法去加载 (关于 findClass() 稍后会进一步介绍). 从 loadClass 实现也可以知道如果不想重新定义加载类的规则, 也没有复杂的逻辑, 只想在运行时加载自己指定的类, 那么我们可以直接使用 this.getClass().getClassLoder.loadClass("className"), 这样就可以直接调用 ClassLoader 的 loadClass 方法获取到 class 对象.
findClass(String)
在 JDK1.2 之前, 在自定义类加载时, 总会去继承 ClassLoader 类并重写 loadClass 方法, 从而实现自定义的类加载类, 但是在 JDK1.2 之后已不再建议用户去覆盖 loadClass()方法, 而是建议把自定义的类加载逻辑写在 findClass()方法中, 从前面的分析可知, findClass()方法是在 loadClass()方法中被调用的, 当 loadClass()方法中父加载器加载失败后, 则会调用自己的 findClass()方法来完成类加载, 这样就可以保证自定义的类加载器也符合双亲委托模式. 需要注意的是 ClassLoader 类中并没有实现 findClass()方法的具体代码逻辑, 取而代之的是抛出 ClassNotFoundException 异常, 同时应该知道的是 findClass 方法通常是和 defineClass 方法一起使用的(稍后会分析)
defineClass(byte[] b, int off, int len)
defineClass()方法是用来将 byte 字节流解析成 JVM 能够识别的 Class 对象 (ClassLoader 中已实现该方法逻辑), 通过这个方法不仅能够通过 class 文件实例化 class 对象, 也可以通过其他方式实例化 class 对象, 如通过网络接收一个类的字节码, 然后转换为 byte 字节流创建对应的 Class 对象, defineClass() 方法通常与 findClass()方法一起使用, 一般情况下, 在自定义类加载器时, 会直接覆盖 ClassLoader 的 findClass()方法并编写加载规则, 取得要加载类的字节码后转换成流, 然后调用 defineClass()方法生成类的 Class 对象
resolveClass(Class<?> c)
使用该方法可以使用类的 Class 对象创建完成也同时被解析. 前面我们说链接阶段主要是对字节码进行验证, 为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用.
四, 热部署
对于 Java 应用程序来说, 热部署就是在运行时更新 Java 类文件.
1, 热部署的原理是什么
想要知道热部署的原理, 必须要了解 java 类的加载过程. 一个 java 类文件到虚拟机里的对象, 要经过如下过程.
首先通过 java 编译器, 将 java 文件编译成 class 字节码, 类加载器读取 class 字节码, 再将类转化为实例, 对实例 newInstance 就可以生成对象.
类加载器 ClassLoader 功能, 也就是将 class 字节码转换到类的实例.
在 java 应用中, 所有的实例都是由类加载器, 加载而来.
一般在系统中, 类的加载都是由系统自带的类加载器完成, 而且对于同一个全限定名的 java 类(如 com.csiar.soc.HelloWorld), 只能被加载一次, 而且无法被卸载.
这个时候问题就来了, 如果我们希望将 java 类卸载, 并且替换更新版本的 java 类, 该怎么做呢?
既然在类加载器中, java 类只能被加载一次, 并且无法卸载. 那是不是可以直接把类加载器给换了? 答案是可以的, 我们可以自定义类加载器, 并重写 ClassLoader 的 findClass 方法. 想要实现热部署可以分以下三个步骤:
销毁该自定义 ClassLoader
更新 class 类文件
创建新的 ClassLoader 去加载更新后的 class 类文件.
2, 热部署与热加载
2.1,Java 热部署与 Java 热加载的联系和区别
Java 热部署与热加载的联系
不重启服务器编译 / 部署项目
基于 Java 的类加载器实现
Java 热部署与热加载的区别
部署方式
热部署在服务器运行时重新部署项目
热加载在运行时重新加载 class
实现原理
热部署直接重新加载整个应用
热加载在运行时重新加载 class
使用场景
热部署更多的是在生产环境使用
热加载则更多的实在开发环境使用
3, 相关代码
User 没有被修改类
- public class User {
- public void add() {
- System.out.println("addV1, 没有修改过...");
- }
- }
User 更新类
- public class User {
- public void add() {
- System.out.println("我把之前的 user add 方法修改啦!");
- }
- }
自定义类加载器
- public class MyClassLoader extends ClassLoader {
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- try {
- // 文件名称
- String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
- // 获取文件输入流
- InputStream is = this.getClass().getResourceAsStream(fileName);
- // 读取字节
- byte[] b = new byte[is.available()];
- is.read(b);
- // 将 byte 字节流解析成 jvm 能够识别的 Class 对象
- return defineClass(name, b, 0, b.length);
- } catch (Exception e) {
- throw new ClassNotFoundException();
- }
- }
- }
更新代码
- public class Hotswap {
- public static void main(String[] args)
- throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException,
- SecurityException, IllegalArgumentException, InvocationTargetException, InterruptedException {
- loadUser();
- System.gc();
- Thread.sleep(1000);// 等待资源回收
- // 需要被热部署的 class 文件
- File file1 = new File("F:\\test\\User.class");
- // 之前编译好的 class 文件
- File file2 = new File(
- "F:\\test\\test\\target\\classes\\com\\itmayiedu\\User.class");
- boolean isDelete = file2.delete();// 删除旧版本的 class 文件
- if (!isDelete) {
- System.out.println("热部署失败.");
- return;
- }
- file1.renameTo(file2);
- System.out.println("update success!");
- loadUser();
- }
- public static void loadUser() throws ClassNotFoundException, InstantiationException, IllegalAccessException,
- NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
- MyClassLoader myLoader = new MyClassLoader();
- Class<?> class1 = myLoader.findClass("com.test.User");
- Object obj1 = class1.newInstance();
- Method method = class1.getMethod("add");
- method.invoke(obj1);
- System.out.println(obj1.getClass());
- System.out.println(obj1.getClass().getClassLoader());
- }
- }
个人博客 蜗牛 https://www.codeobj.com/
来源: http://www.bubuko.com/infodetail-3345666.html