1 类加载器的分类

  • 启动类加载器(Bootstrap ClassLoader):主要负责加载<JAVA_HOME>\lib目录中的,或是-Xbootclasspath参数指定的路径中的,并且可以被虚拟机识别(仅仅按照文件名识别的)的类库到虚拟机内存中。它加载的是System.getProperty("sun.boot.class.path")所指定的路径jar
  • 扩展类加载器(Extension ClassLoader):主要负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。它加载的是System.getProperty("java.ext.dirs")所指定的路径jar
  • 应用程序类加载器(Application ClassLoader):也叫系统类加载器,主要负责加载ClassPath路径上的类库,如果应用程序没有自定义类加载器,则这个就是默认的类加载器。它加载的是System.getProperty("java.class.path")所指定的路径jar

    2 类加载器的特点

  • 运行一个程序时,总是由Application Loader(系统类加载器)开始加载指定的类。

  • 在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
  • Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null

    3 类加载器的双亲委派模型

    3.1 类加载器双亲委派模型的工作过程

    如果一个类加载器收到一个类加载的请求,它首先将这个请求委派给父类加载器去完成,每一个层次类加载器都是如此,则所有的类加载请求都会传送到顶层的启动类加载器,只有父加载器无法完成这个加载请求(即它的搜索范围中没有找到所要的类),子类才尝试加载。

    3.2 使用双亲委派模型主要是两个原因:

  • 可以避免重复加载,当父类已经加载了,子类不需再次加载;

  • 安全因素,如果不用这种,则用户可以随意的自定义加载器来替代Java核心API,则就会带来安全隐患。

下面是一个类加载器双亲委派模型,这里各个类加载器并不是继承关系,它们利用组合实现的父类与子类关系。

classloader.png
双亲委派模型

4 类加载的几种方式

  • 命令行启动应用时候由JVM初始化加载含有main的主类。
  • 通过Class.forName("Hello")方法动态加载类,默认会执行初始化块,这是因为Class.forName("Hello")其实就是Class.forName("Hello",true,CALLCLASS.getClassLoader()),第二个参数就是类加载过程中的连接操作。
  • 通过ClassLoader.loadClass("Hello")方法动态加载类,不会执行初始化块,因为loadClass方法有两个参数,用户只是用第一个参数,第二个参数默认为false,即不对该类进行解析则就不会初始化。
  1. public Class<?> loadClass(String name) throws ClassNotFoundException {
  2. return loadClass(name, false);
  3. }

5 类加载实例

当在命令行下执行:java HelloWorld(HelloWorld是含有main方法的类的Class文件),JVM会将HelloWorld.class加载到内存中,并在堆中形成一个Class的对象HelloWorld.class

基本的加载流程如下:

  • 寻找jre目录,寻找jvm.dll,并初始化JVM
  • 产生一个Bootstrap Loader(启动类加载器);
  • Bootstrap Loader,该加载器会加载它指定路径下的Java核心API,并且再自动加载Extension Loader(标准扩展类加载器),Extension Loader会加载指定路径下的扩展Java API,并将其父Loader设为Bootstrap Loader;
  • Bootstrap Loader也会同时自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extension Loader
  • 最后由AppClass Loader加载CLASSPATH目录下定义的类,HelloWorld类

    6 类加载器与类的唯一性

    类加载器虽然只用于实现类的加载动作,但是对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性。通俗的说,JVM中两个类是否“相等”,首先就必须是同一个类加载器加载的,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要类加载器不同,那么这两个类必定是不相等的。

这里的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

以下代码说明了不同的类加载器对instanceof关键字运算的结果的影响。

  1. package com.lannister.java.demo.jvm;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. /**
  5. * 类加载器在类相等判断中的影响
  6. *
  7. * instanceof关键字
  8. *
  9. */
  10. public class ClassLoaderDemo {
  11. public static void main(String[] args) throws Exception {
  12. // 使用ClassLoaderTest的类加载器加载本类
  13. Object obj1 = ClassLoaderDemo.class.getClassLoader().loadClass("com.lannister.java.demo.jvm.ClassLoaderDemo").newInstance();
  14. System.out.println(obj1.getClass());
  15. System.out.println(obj1 instanceof com.lannister.java.demo.jvm.ClassLoaderDemo);
  16. // 使用自定义类加载器加载本类
  17. Object obj2 = new MyClassLoader().loadClass("com.lannister.java.demo.jvm.ClassLoaderDemo").newInstance();
  18. System.out.println(obj2.getClass());
  19. System.out.println(obj2 instanceof com.lannister.java.demo.jvm.ClassLoaderDemo);
  20. }
  21. }
  22. class MyClassLoader extends ClassLoader{
  23. @Override
  24. public Class<?> loadClass(String name) throws ClassNotFoundException {
  25. try {
  26. String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
  27. InputStream is = getClass().getResourceAsStream(fileName);
  28. if (is == null) {
  29. return super.loadClass(name);
  30. }
  31. byte[] b = new byte[is.available()];
  32. is.read(b);
  33. return defineClass(name, b, 0, b.length);
  34. } catch (IOException e) {
  35. throw new ClassNotFoundException();
  36. }
  37. }
  38. }
  39. /*
  40. Class.getResourceAsStream(String path)
  41. path 不以’/'开头时默认是从此类所在的包下取资源,以’/'开头则是从ClassPath根下获取。
  42. 其只是通过path构造一个绝对路径,最终还是由ClassLoader获取资源。
  43. */

输出结果:

  1. class com.lannister.java.demo.jvm.ClassLoaderDemo
  2. true
  3. class com.lannister.java.demo.jvm.ClassLoaderDemo
  4. false
  • new MyClassLoader()是自定义的类加载器,可以用来加载与当前类在同一路径下的Class文件。
  • main函数的第一部分使用系统加载主类ClassLoaderDemo的类加载器加载ClassLoaderDemo,输出显示,obj1的所属类型检查正确,这时虚拟机中有2个ClassLoaderDemo类,一个是主类,另一个是main()方法中加载的类,由于这两个类使用同一个类加载器加载并且来源于同一个Class文件,因此这两个类是完全相同的。
  • 第二部分使用自定义的类加载器加载ClassLoaderDemo,输出显示,obj2确实是类com.lannister.java.demo.jvm.ClassLoaderDemo实例化出来的对象,但是第二句输出false。此时虚拟机中有3个ClassLoaderDemo类,由于第3个类的类加载器与前面2个类加载器不同,虽然来源于同一个Class文件,但它是一个独立的类,所属类型检查是返回结果自然是false。

    7 创建自己的类加载器

    Java应用开发过程中,可能会需要创建应用自己的类加载器。典型的场景包括实现特定的Java字节代码查找方式、对字节代码进行加密/解密以及实现同名Java类的隔离等。创建自己的类加载器并不是一件复杂的事情,只需要继承自java.lang.ClassLoader类并覆写对应的方法即可。 java.lang.ClassLoader中提供的方法有不少,下面介绍几个创建类加载器时需要考虑的:

  • defineClass():这个方法用来完成从Java字节码的字节数组到java.lang.Class的转换。这个方法是不能被覆写的,一般是用原生代码来实现的。

  • findLoadedClass():这个方法用来根据名称查找已经加载过的Java类。一个类加载器不会重复加载同一名称的类。
  • findClass():这个方法用来根据名称查找并加载Java类。当自定义类加载器,加载ClassPath路径以外的类时,可以改写这个方法,不会破坏双亲委派模型
  • loadClass():这个方法用来根据名称加载Java类。通过改写这个方法,可以破坏双亲委派模型。
  • resolveClass():这个方法用来连接一个Java类
  1. //双亲委派模型的实现
  2. protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
  3. //check the class has been loaded or not
  4. Class c = findLoadedClass(name);
  5. if(c == null){
  6. try{
  7. if(parent != null){
  8. //默认情况下,这里的parent指的是应用程序类加载器
  9. c = parent.loadClass(name,false);
  10. }else{
  11. c = findBootstrapClassOrNull(name);
  12. }
  13. }catch(ClassNotFoundException e){
  14. //if throws the exception ,the father can not complete the load
  15. }
  16. if(c == null){
  17. c = findClass(name);
  18. }
  19. }
  20. if(resolve){
  21. resolveClass(c);
  22. }
  23. return c;
  24. }
  25. //父加载器没能成功加载,调用自己的findClass()方法进行加载
  26. protected Class<?> findClass(String name) throws ClassNotFoundException {
  27. throw new ClassNotFoundException(name);
  28. }