引言
当刚学 c/c++ 程序时,第一次在控制台下运行程序,需要将所有程序文件进行编译、链接,然后再运行。这很容易理解,毕竟我在一段程序中引用了另一个文件中定义的函数,当然要把这些文件连接到一起。但是在运行 java 程序时却不需要这么做了,顶多在运行时加一个 -classpath 参数。这背后的原因就是 jvm 和 java 中的 ClassLoader 机制。
什么是 ClassLoader
当 c/c++ 运行时,是将代码编译成机器码,链接成可运行文件扔给 cpu 运行,但是 java 不是这么做的,它的运行是在 jvm 中的,而关键是 java 并不是把所有要运行的代码打包一起扔给 jvm ,jvm 内部刚开始只加载了你 java XXX 命令中指定的那个初始类 (有 mian 函数的那个类),在这个类运行过程中需要其他类时,再使用类加载器将需要的类(.class 文件)加载到 jvm 中。到这里便引出了三问题:
- 类是经过哪些过程加载到 jvm 中的 ?
- 什么时候是"需要其他类的时候" ?
- 所谓的"类加载器"具体到代码到底是个啥玩意 ?
类是经过哪些过程加载到 jvm 中的 ?
一 、根据类名在指定的路径下找到对应的 .class 文件
二、 解析 class 文件,在方法区中(不了解方法区可以先看看 jvm 内存模型) 分配空间,并创建 Class 对象:Class 对象就是 Class 类型的对象 (类可以创建对象,而类本身就是一个 Class 类型的对象 ,它是保存着类的 类型信息 的对象。)深入请看https://blog.csdn.net/bingduanlbd/article/details/8424243
三、 对 Class 对象进行一系列初始化操作 (静态变量赋值,执行 static 代码块.....)
(在其他博客和书中会将这个加载过程分为五个步骤,但是要介绍这些步骤即会扯到文件格式验证,引用解析等一堆东西,这篇文章旨在简单介绍,不予赘述。)
什么时候是"需要其他类的时候" ?
一 、 使用 new 关键字实例化对象的时候、读取或设置一个静态字段 ( final 修饰除外),调用一个静态方法时。
二 、 使用反射加载一个类时
三、 加载一个类时它的父类还没有加载
四、 启动时指定的类,就是引言中所说的那种情况
(还有一种是与 java 对动态类型的支持相关的,并不常见,可以参考《深入理解 java 虚拟机》)
所谓的"类加载器"具体到代码到底是个啥玩意 ?
java 已经定义好的类加载器有:启动(Bootstrap)类加载器 ,扩展(Extension)类加载器,系统(App)类加载器 ,安全(Secure)类加载器 ,URL类加载器 它们的继承关系是这样的:
其中 ClassLoader 是一个抽象类,定义了类加载器的基本工作机制,SecureClassLoader 在此基础上支持了一些权限控制相关操作,URLClassLoader 则又加入了一些方便定义加载路径的接口,其余的三个类加载器 BootstarpClassLoader,AppClassLoader,ExtClassLoader 都会由 jvm 创建出实际的对象,完成实际的功能 (下文有介绍)。
分析 ClassLoader 的源码可以大致了解类加载器的工作原理,这里不贴源码分析了,介绍一下它的主要方法和调用关系,源码可以自己看。
ClassLoader 中与加载类相关的方法:
方法 | 说明 |
---|---|
loadClass(String name) | 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。 |
findClass(String name) | 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。 |
findLoadedClass(String name) | 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
调用关系:
整个的过程就是 :
一、先在缓存中找,已经加载过的就不加载了,直接返回 。
二、没有在缓存中找到,调用"父类"的 loadClass() 方法,委托"父类"加载,这就是所谓的双亲委派模型。加引号是因为不是真正的父类,而是它在一个的字段 parent 中保存的父类加载器,在 jvm 创建加载器的对象时指定,与上面继承关系图中表示的不同,委托关系图如下:
如果你想自定义一个类加载器用来加载你特殊文件路径下的类,或是要对加密字节码文件进行解码,可以继承 ClassLoader ,SecureClassLoader,URLClassLoader 中的一个,覆写 findClass() 方法,大多数情况下,继承自 URLClassLoader 最为简单。
三、"父类"加载失败,调用具体加载器自己定义的 findClass(String name) 函数比如 ExtClass-Loader 的 findClass() 执行的就是去 <JAVA_HOME>/lib/ext 这个路径下查找 class 文件,而 BootstarpClassLoader 则是去 <JAVA_HOME>/lib 下去查找,它们俩个都负责加载一些 java 程序运行需要的基础组件,然后是AppClassLoader,它负责到你的项目路径中加载你写的项目。如果你要自己写一个类加载器,应该继承自 URLClassloader ,然后重写它的 findClass() 方法,如果在构造方法中不指定"父类"加载器,则它的默认"父类"就是 AppClassLoader 。
(在看源码时你可能会发现 ExtClassLoader 甚至连 findClass()这个函数都没有,那是因为它继承自 URLClassLoader, 直接用路径完成父类构造,然后调用父类的 findClass() 就可以了,按上文理解也没有什么问题)
四、找到了 class 文件把字节码读进来,还要用 defineClass() 将字节码转变为真正的 Class 对象,也就是将字节码载入 jvm。
五、对生成的 Class 对象进行最后的链接操作,链接类或接口包括验证和准备类或接口、它的直接父类、它的直接父接口、它的元素类型(如果是一个数组类型)及其它必要的动作。
双亲委派模型的优点
一、主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类。
二、同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
破坏双亲委派模型和线程上下文类加载器 待续.....
网友评论