序言
梦里玉人方下马,恨他天外一声鸿。
今天来总结Java类加载机制。
编译器与解释器
我们通常会把编程语言的处理器分为编译器
和解释器
。
编译器则是将某种语言代码转换为另外一种语言的程序,通常会转换为机器语言。
解释器是一种用来执行程序的软件,它会根据程序代码中的算法执行运算,如果这个软件是根据虚拟的或者类似机器语言的程序设计语言写成,那也称为虚拟机。
Java会混用解释器和编译器,Java会先通过编译器将源代码转换为Java二进制代码(字节码),并将这种虚拟的机器语言保存在文件中(通常是.class文件),之后通过Java虚拟机(JVM)的解释器来执行这段代码。
类和类加载器
这里直接上《深入理解Java虚拟机》原文,写得很好:
对于任意的一个类。其实都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。两个类是否相等,其实只有这两个类都是由同一个类加载器加载的前提下才有意义。只要加载它们的类加载器不同,那么这两个类就必定不相等。
Java是面向对象的语言,字节码中包含了很多Class信息。在 JVM 解释执行的过程中,ClassLoader就是用来加载Java类的,它会将Java字节码加载到内存中。每个 Class 类对象的内部都有一个 classLoader 属性来标识自己是由哪个 ClassLoader 加载的。
Java的类加载体系:parent属性 理论关系
1 | Bootstrap Classloader -> Extension ClassLoader -> Application ClassLoader |
JDK类实现体系:实现/继承的关系,通常继承URLClassLoader
1 | ClassLoader -> Secure ClassLoader -> URL ClassLoader -> ExtClassLoader/AppClassLoader |
类加载层次
站在JVM角度来看,只存在两种不同的类加载器:
- 启动类加载器 BootstrapClassLoader 使用C++实现
- 其他类加载器/都是由Java实现/全部继承抽象类java.lang.ClassLoader
Bootstrap ClassLoader 启动类加载器
负责加载存放在JAVA_HOME/lib
目录下,或者被-Xbooclasspath参数所指定的路径中存放的,并且可以被JVM识别的类库,加载到虚拟机的内存中。
底层原生代码是C++语言编写,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。
出于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类。
它负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.. 都在里面。这个 ClassLoader 比较特殊,它其实不是一个ClassLoader实例对象,而是由C代码实现。用户在实现自定义类加载器时,如果需要把加载请求委派给启动类加载器,那可以直接传入null作为 BootstrapClassLoader。
Extension ClassLoader 扩展类加载器
负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/
目下的所有jar,库名通常以 javax 开头。
Application ClassLoader 系统类加载器
直接提供给用户使用的ClassLoader,它会加载 CLASSPATH 环境变量或者 java.class.path 属性里定义的路径中的 jar 包和目录,负责加载包括开发者编写的代码、以及依赖的第三方库中的类。
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
可以通过 ClassLoader.getSystemClassLoader() 来获取它。
特殊:URLClassloader
ClassLoader抽象类的一种实现,它可以根据URL搜索类或资源,并进行远程加载。在JDK实现中,BootstrapClassLoader、ExtClassLoader、AppClassLoader等都是 URLClassLoader 的子类。
ExtClassLoader 和 AppClassLoader 类的实现代码位于rt.jar 中的 sun.misc.Launcher 类中,Launcher是由BootstrapClassLoader加载的。
双亲委派机制
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
ClassLoader类 - 核心方法
每个 ClassLoader 对象都是一个 java.lang.ClassLoader 的实例。每个Class对象都被这些 ClassLoader 对象所加载,通过继承java.lang.ClassLoader 可以扩展出自定义 ClassLoader,并使用这些自定义的 ClassLoader 对类进行加载。
1 | package java.lang; |
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类) 接收全类名字符串,在内存中加载为Class 对象,返回。findClass
(查找指定的Java类) 查找当前JVM中,名称为name的Class类对象是否存在,找到的话返回Class类对象。findLoadedClass
(查找JVM已经加载过的类)defineClass
(定义一个Java类) 接受字节数组,在内存中加载为Class对象,返回。resolveClass
(链接指定的Java类)getParent
返回其parent ClassLoader
重点逻辑:
loadClass(String classname),参数为需要加载的全限定类名,该方法会先查看目标类是否已经被加载,查看父级加载器并递归调用loadClass(),如果都没找到则调用findClass()。
findClass(),搜索类的位置,一般会根据名称或位置加载.class字节码文件,获取字节码数组,然后调用defineClass()。
defineClass(),将字节码转换为 JVM 的 java.lang.Class 对象。
嵌套调用关系:loadClass->findClass->defineClass
整个demo玩玩看;
1 | public class ClassLoaderTest { |
输出:
重点:loadClass()
源码:
1 | protected Class<?> loadClass(String name, boolean resolve) |
它先使用了findLoadedClass(String)方法来检查这个类是否被加载过
接着使用父加载器调用loadClass(String)方法
之后就调用自身findClass(String) 方法装载类。
最后通过上述步骤找到了对应的Class对象,并且接收到的resolve参数的值为true,那么就调用resolveClass(Class)方法来链接(验证,准备,解析)。
forName方法和loadClass方法的区别
两种类加载:显式、隐式
显式加载[动态加载]:
- Java反射
CLass.forName("com.sec.Test")
加载、链接、初始化 - 类加载器
ClassLoader.loadClass("com.sec.Test")
:仅加载
隐式加载[静态加载]:
- 创建类对象 new
- 使用类的静态域
- 创建子类对象
- 使用子类的静态域
- 调用类的静态方法
Class.method()
Demo:
1 | // 反射加载Test示例 显式加载 |
Class.forName("类名")
默认会初始化被加载类的静态属性和方法
如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器)
ClassLoader.loadClass()
只是将类加载进JVM虚拟机。
Class.forName()
可以加载数组,而ClassLoader.loadClass()
不能。
子类引用父类的静态字段,不会触发子类的初始化。
自定义ClassLoader
Java 中提供的默认 ClassLoader 只加载指定目录下面的 jar 和 class ,我们从上面了解到 ClassLoader是一个抽象类,实现自定义的 ClassLoader 需要继承该类并实现里面的方法。
java.lang.ClassLoader
是所有的类加载器的父类。
一般情况下,我们重写父类的 findClass 方法即可。
双亲委派机制是loadClass()函数负责的。 可以打破但是没必要。
*一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。 *
ClassLoader 方法那么多为什么只重写 findClass 方法? 因为 JDK 已经在 loadClass 方法中帮我们实现了 ClassLoader 搜索类的算法,当在 loadClass 方法中搜索不到类时,loadClass 方法就会调用findClass 方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写 loadClass 搜索类的算法。
步骤:
- 编写一个类继承自ClassLoader抽象类。
- 覆盖它的
findClass()
方法。 - 在
findClass()
方法中调用defineClass()
。
自定义 ClassLoader DEMO
假如我们自定义一个 classloader,我们可以编写一个测试类来说明。在当前目录下面新建一个 Hello 类。里面有个方法 sayHello,然后放入到指定目录下面,如:我当前的目录为:
1 | public class Hello { |
接着我们需要自定义一个 ClassLoader 来继承系统的 ClassLoader,命名为 DIYClassLoader 类。
1 | public class DIYClassLoader extends ClassLoader{ |
现在需要的是写一个调用方法:
1 | public static void main(String[] args) { |
最终实现方法调用:
重要方法 loadClass
直接看源码:
1 | protected Class<?> loadClass(String name, boolean resolve) |
上面是方法原型,一般实现这个方法的步骤是
- 执行
findLoadedClass(String)
去检测这个class是不是已经加载过了,已经加载过的都应该在缓存中。 - 执行父加载器的
loadClass
方法。如果父加载器为空,则Bootstrap ClassLoader加载器去加载。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。 - 如果向上委托父加载器没有加载成功,则通过
findClass(String)
查找。
如果class在上面的步骤中找到了,参数resolve又是true的话(上文提到的==resolveClass==,resolve参数就是表示需不需要连接阶段),那么loadClass()
又会调用resolveClass(Class)
这个方法去链接,来生成最终的Class对象。
类加载主要的三个阶段
加载
链接
- 验证:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
- 准备:为类变量分配内存,“预分配内存”,并且赋予初值0。
- 解析:将常量池中的符号引用转换为直接引用(内存块),替换为具体的内存地址或偏移量。
初始化:只给static修饰的变量或者语句赋值,执行静态代码块。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
URLClassLoader
在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它继承了ClassLoader,能够从本地或者网络上指定的位置加载类,我们可以使用该类作为自定义的类加载器使用。
URLClassLoader是ClassLoader的子类,它用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源。也就是说,通过URLClassLoader就可以加载指定jar中的class到内存中。
下面来看一个例子,在该例子中,我们要完成的工作是利用URLClassLoader加载jar并运行其中的类的某个方法。
加载过程中不会执行静态代码块。
构造方法:
public URLClassLoader(URL[] urls)
:指定要加载的类的所在地URL地址,父类加载器默认为App系统类加载器
public URLClassLoader(URL[] urls,ClassLoader parent)
:指定要加载的类的所在的URL地址,并指定父类加载器。
1 | File file = new File(jar文件全路径); |
Java 运行时类加载
ClassLoader.loadClass()与Class.forName()是反射用来构造类,给一个类名即可,返回值是Class。
注意一点,C lass.forName()方法全套服务,会执行目标class的static代码块方法。
loadClass 应该在URLClassLoader里面用的多,这个涉及到动态加载jar包。
获取当前ClassLoader的四种方式:
1 | // 方式一:获取当前类的 ClassLoader |
通过反射获取defineClass方法
1 | ClassLoader.defineClass(buye[] b); |
1 | 1. java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class}); |
通过反射结合Thread
1 | 1. ... |
构造函数
类加载完成后,对象实例化是Class.newInstance()方法。该方法会执行无参构造函数,
凡是能触发构造函数,都能触发静态代码块。