ClassLoader理解及使用

4,083 阅读13分钟

ClassLoader是什么?

ClassLoader是类加载器,作用是将class文件加载到jvm虚拟机中去。并不会一次性加载所有的class文件,而是根据需要去动态加载。

每个类都会关联一个class loader。

* <p> Every {@link Class <tt>Class</tt>} object contains a {@link
* Class#getClassLoader() reference} to the <tt>ClassLoader</tt> that defined
* it.
public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {

    /** defining class loader, or null for the "bootstrap" system loader. */
    private transient ClassLoader classLoader;
public ClassLoader getClassLoader() {
    if (isPrimitive()) {
        return null;
    }
    return (classLoader == null) ? BootClassLoader.getInstance() : classLoader;
}

Java类加载流程

Java系统级有三个类加载器:  

BootstrapClassLoader 最顶层的加载类,使用C/C++编写,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。 

ExtentionClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下或者-Djava.ext.dirs选项指定目录下的jar包和class文件。

 AppClassLoader  查找当前应用的Classpath目录下或者-Djava.class.path选项所指定的目录下的jar包和Class文件。项目中的自定义类编译后都会放到Classpath目录。

自定义ClassLoader :开发者可以自定义classloader。自定义类加载器通过继承java.lang.ClassLoader类的方式来实现自己的类加载器

Java中ClassLoader的继承关系

运行一个Java程序需要用到几种类型的类加载器呢?如下所示。

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader loader = ClassLoaderTest.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}

打印结果如下

sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742

第1行说明加载ClassLoaderTest的类加载器是AppClassLoader,第2行说明AppClassLoader的父加载器为ExtClassLoader。至于为何没有打印出ExtClassLoader的父加载器Bootstrap ClassLoader,这是因为Bootstrap ClassLoader是由C/C++编写的,并不是一个Java类,因此我们无法在Java代码中获取它的引用。

系统所提供的类加载器有3种类型,但是系统提供的ClassLoader相关类却不只3个。另外,AppClassLoader的父类加载器为ExtClassLoader,并不代表AppClassLoader继承自ExtClassLoader,ClassLoader的继承关系如下所示

VmZfXD.png

可以看到上图中共有5个ClassLoader相关类,下面简单对它们进行介绍:

  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。
  • SecureClassLoader继承了抽象类ClassLoader,但SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。
  • ExtClassLoader和AppClassLoader都继承自URLClassLoader,它们都是Launcher 的内部类,Launcher 是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。

加载方式-双亲委托代理模式

自下而上进行委托,再自上而下进行查找

类加载器查找Class所采用的是双亲委托模式,所谓双亲委托模式就是首先判断该Class是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了该Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找。

VmZ4ne.png

 双亲委托模式的好处

  1. 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。
  2. 更加安全,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类,除非我们修改
    类加载器搜索类的默认算法。还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类,想要骗过Java虚拟机显然不会那么容易。

自定义ClassLoader

系统提供的类加载器只能够加载指定目录下的jar包和Class文件,如果想要加载网络上的或者是D盘某一文件中的jar包和Class文件则需要自定义ClassLoader。

实现自定义ClassLoader需要两个步骤:

  1. 定义一个自定义ClassLoade并继承抽象类ClassLoader。
  2. 复写findClass方法,并在findClass方法中调用defineClass方法。

编写测试Class文件

public class Jobs {
 private String word = "One more thing";
 public Jobs(String s){
     word = s;
 }
 public Jobs(){
 }
    private void say() {
        System.out.println(word);
    }
}

将这个Jobs.java放入到D:\lib中,使用cmd命令进入D:\lib目录中,执行Javac Jobs.java对该java文件进行编译,这时会在D:\lib中生成Jobs.class

编写自定义ClassLoader

public class DiskClassLoader extends ClassLoader{
	private String path;
	
	public DiskClassLoader(String path) {
	this.path = path;
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		Class clazz = null;
		byte[] classData = loadClassData(name);//获取class文件的字节码数组
		if(classData == null){
			throw new ClassNotFoundException();
		}else {
			//调用defineClass方法将class文件的字节码数组转为class类实例
			clazz = defineClass(name, classData, 0, classData.length);
		}
		return clazz;
	}
	
	private byte[] loadClassData(String name) {
		String fileName = getFileName(name);
		File file = new File(path,fileName);
		InputStream in= null;
		ByteArrayOutputStream  out = null;
		try {
			in = new FileInputStream(file);
			out = new ByteArrayOutputStream();
			byte[] buffer = new byte[1024];
			int length = 0;
			while ((length = in.read(buffer))!= -1) {
				out.write(buffer,0,length);
			}
		
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
                if(in!=null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try{
                if(out!=null) {
                    out.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
		}
		if(out!=null){
			return out.toByteArray();
		}else {
			return null;
		}
	}
	
	private String getFileName(String name){
		int index = name.lastIndexOf('.');
		if(index == -1){//没找到.
			return name+".class";
		}else {
			return name.substring(index+1)+".class";
		}
	}
}

验证DiskClassLoader是否可用,代码如下所示

public class ClassLoaderTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		DiskClassLoader diskClassLoader = new DiskClassLoader("D:\\lib");
		try {
			//注意这个类名不能在项目工程中存在,因为在项目工程中找个类名对应的类的加载器是AppClassLoader,因为运行是会编译生成Test.class,
			//并双亲委托机制中在AppClassLoader层就从缓存中找到了
			Class c = diskClassLoader.loadClass("Jobs");//参数是class名字,不带后缀
			if (c !=null) {
				try {
					Object o = c.newInstance();//Class.newInstance()只能使用无参构造方法创建对象 
					
					Constructor constructor = c.getDeclaredConstructor(String.class);
					Object object = constructor.newInstance("hahah");
					ClassLoader classLoader = object.getClass().getClassLoader();
					while (classLoader!=null) {
						System.out.println(classLoader.toString());
						classLoader = classLoader.getParent();
					}
					
					Method sayMethod = c.getDeclaredMethod("say", null);
					sayMethod.setAccessible(true);
					sayMethod.invoke(object, null);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

Android ClassLoader类加载类分析

* <p> The <tt>ClassLoader</tt> class uses a delegation model to search for
* classes and resources.  Each instance of <tt>ClassLoader</tt> has an
* associated parent class loader.  When requested to find a class or
* resource, a <tt>ClassLoader</tt> instance will delegate the search for the
* class or resource to its parent class loader before attempting to find the
* class or resource itself.  The virtual machine's built-in class loader,
* called the "bootstrap class loader", does not itself have a parent but may
* serve as the parent of a <tt>ClassLoader</tt> instance.

每个classLoader对象都有父class loader(除了BootClassLoader没有父classloader)。在该类的classloader使用loadClass( )加载类时,会先去它的父classloader中查找。如果该类没有classloader,则使用jvm创建的BootClassLoader赋值。 

/**
 * Loads the class with the specified <a href="#name">binary name</a>.  The
 * default implementation of this method searches for classes in the
 * following order:
 *
 * <ol>
 *
 *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
 *   has already been loaded.  </p></li>
 *
 *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
 *   on the parent class loader.  If the parent is <tt>null</tt> the class
 *   loader built-in to the virtual machine is used, instead.  </p></li>
 *
 *   <li><p> Invoke the {@link #findClass(String)} method to find the
 *   class.  </p></li>
 *
 * </ol>
 *
 * <p> If the class was found using the above steps, and the
 * <tt>resolve</tt> flag is true, this method will then invoke the {@link
 * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
 *
 * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
 * #findClass(String)}, rather than this method.  </p>
 *
 *
 * @param  name
 *         The <a href="#name">binary name</a> of the class
 *
 * @param  resolve
 *         If <tt>true</tt> then resolve the class
 *
 * @return  The resulting <tt>Class</tt> object
 *
 * @throws  ClassNotFoundException
 *          If the class could not be found
 */
// Android-removed: Remove references to getClassLoadingLock
//                   Remove perf counters.
//
// <p> Unless overridden, this method synchronizes on the result of
// {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
// during the entire class loading process.
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);//Bootstrap ClassLoader是由C/C++编写的,是虚拟机的一部分,所以它并不是一个JAVA类
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}
protected final Class<?> findLoadedClass(String name) {
    ClassLoader loader;
    if (this == BootClassLoader.getInstance())
        loader = null;
    else
        loader = this;
    return VMClassLoader.findLoadedClass(loader, name);
}

1)判断该类是否已加载。

findLoadedClass(String name)会返回已经被加载到内存中的Class对象,返回null表示未加载过。

2)递归调用父class loader去委托加载,直到父class loader为空,使用bootClassloader作为当前classloader去加载。

3)最顶层的class loader加载不到(找不到,返回null),则使用当前类自己的class loader(可能有多层,如果每个上层都找不到,最终会回到当前定义类自己的class loader去加载)

Android ClassLoader 继承关系

VnM5ZT.png

  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。BootClassLoader是它的内部类。
  • SecureClassLoader类和JDK8中的SecureClassLoader类的代码是一样的,它继承了抽象类ClassLoader。SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader类和JDK8中的URLClassLoader类的代码是一样的,它继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源
  • InMemoryDexClassLoader是Android8.0新增的类加载器,继承自BaseDexClassLoader,用于加载内存中的dex文件。
  • BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类PathClassLoader和DexClassLoader都继承它。

  • 系统ClassLoader主要有3种分别是BootClassLoader、PathClassLoader和DexClassLoader

    1.1 BootClassLoader

    Android系统启动时会使用BootClassLoader来预加载常用类,与Java中的BootClassLoader不同,它并不是由C/C++代码实现,而是由Java实现的

    class BootClassLoader extends ClassLoader {
    
        private static BootClassLoader instance;
    
        @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
        public static synchronized BootClassLoader getInstance() {
            if (instance == null) {
                instance = new BootClassLoader();
            }
    
            return instance;
        }
    }复制代码

    BootClassLoader是ClassLoader的内部类,并继承自ClassLoader。BootClassLoader是一个单例类,需要注意的是BootClassLoader的访问修饰符是默认的,只有在同一个包中才可以访问,因此我们在应用程序中是无法直接调用的。

    1.2 DexClassLoader

    DexClassLoader可以加载dex文件以及包含dex的压缩文件(apk和jar文件),不管是加载哪种文件,最终都是要加载dex文件,dex文件以及包含dex的压缩文件统称为dex相关文件。 来查看DexClassLoader的代码,如下所示。

    public class DexClassLoader extends BaseDexClassLoader {
    55      public DexClassLoader(String dexPath, String optimizedDirectory,
    56              String librarySearchPath, ClassLoader parent) {
    57          super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    58      }
    59  }
    60 

    DexClassLoader的构造方法有四个参数: dexPath:dex相关文件路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’ optimizedDirectory:解压的dex文件存储路径,这个路径必须是一个内部存储路径,一般情况下使用当前应用程序的私有路径:/data/data/<Package Name>/...。 librarySearchPath:包含 C/C++ 库的路径集合,多个路径用文件分隔符分隔分割,可以为null。 parent:父加载器。 DexClassLoader 继承自BaseDexClassLoader ,方法实现都在BaseDexClassLoader中。

    1.3 PathClassLoader

    Android系统使用PathClassLoader来加载系统类和应用程序的类,来查看它的代码:

    public class PathClassLoader extends BaseDexClassLoader {
    26      /**
    27       * Creates a {@code PathClassLoader} that operates on a given list of files
    28       * and directories. This method is equivalent to calling
    29       * {@link #PathClassLoader(String, String, ClassLoader)} with a
    30       * {@code null} value for the second argument (see description there).
    31       *
    32       * @param dexPath the list of jar/apk files containing classes and
    33       * resources, delimited by {@code File.pathSeparator}, which
    34       * defaults to {@code ":"} on Android
    35       * @param parent the parent class loader
    36       */
    37      public PathClassLoader(String dexPath, ClassLoader parent) {
    38          super(dexPath, null, null, parent);
    39      }
    40  
    41      /**
    42       * Creates a {@code PathClassLoader} that operates on two given
    43       * lists of files and directories. The entries of the first list
    44       * should be one of the following:
    45       *
    46       * <ul>
    47       * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
    48       * well as arbitrary resources.
    49       * <li>Raw ".dex" files (not inside a zip file).
    50       * </ul>
    51       *
    52       * The entries of the second list should be directories containing
    53       * native library files.
    54       *
    55       * @param dexPath the list of jar/apk files containing classes and
    56       * resources, delimited by {@code File.pathSeparator}, which
    57       * defaults to {@code ":"} on Android
    58       * @param librarySearchPath the list of directories containing native
    59       * libraries, delimited by {@code File.pathSeparator}; may be
    60       * {@code null}
    61       * @param parent the parent class loader
    62       */
    63      public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    64          super(dexPath, null, librarySearchPath, parent);
    65      } 复制代码

    PathClassLoader继承自BaseDexClassLoader,实现也都在BaseDexClassLoader中。 PathClassLoader的构造方法中没有参数optimizedDirectory,这是因为PathClassLoader已经默认了参数optimizedDirectory的值为:/data/dalvik-cache,很显然PathClassLoader无法定义解压的dex文件存储路径,因此PathClassLoader通常用来加载已经安装的apk的dex文件(安装的apk的dex文件会存储在/data/dalvik-cache中)。

    DexClassLoader和PathClassLoader区别

    DexClassLoader可以加载任何路径的apk/dex/jar 。

    PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。

    这个也是PathClassLoader作为默认的类加载器的原因,因为一般程序都是安装了,在打开,这时候PathClassLoader就去加载指定的apk(解压成dex,然后在优化成odex)就可以了

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
    63              String librarySearchPath, ClassLoader parent) {
    64          super(parent);
    65          this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
    66  
    67          if (reporter != null) {
    68              reporter.report(this.pathList.getDexPaths());
    69          }
    70      }

    dexPathList是dex文件路径集合类,其成员变量dexElements是所有dexFile集合

      final class DexPathList {   private Element[] dexElements;public DexPathList(ClassLoader definingContext, String dexPath,
    128              String librarySearchPath, File optimizedDirectory) {
    156          // save dexPath for BaseDexClassLoader
    157          this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
    158                                             suppressedExceptions, definingContext);
    
    184      }
    }

    BaseClassLoader 通过loadClass加载类后,会调用findClass方法

     @Override
    89      protected Class<?> findClass(String name) throws ClassNotFoundException {
    90          List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    91          Class c = pathList.findClass(name, suppressedExceptions);
    100          return c;
    101      }

    DexPathList中findClass方法如下

      public Class<?> findClass(String name, List<Throwable> suppressed) {
    465          for (Element element : dexElements) {
    466              Class<?> clazz = element.findClass(name, definingContext, suppressed);
    467              if (clazz != null) {
    468                  return clazz;
    469              }
    470          }
    475          return null;
    476      }

    Element类时DexPathList的内部类

     static class Element {
    564          private final File path;
    566          private final DexFile dexFile;
    568          private ClassPathURLStreamHandler urlHandler;
    public Class<?> findClass(String name, ClassLoader definingContext,
    676                  List<Throwable> suppressed) {
    677              return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
    678                      : null;
    679          }
    }
    

    最终是调用DexFile的loadClassBinaryName方法

     public Class loadClass(String name, ClassLoader loader) {
    263          String slashName = name.replace('.', '/');
    264          return loadClassBinaryName(slashName, loader, null);
    265      }
    
     public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    275          return defineClass(name, loader, mCookie, this, suppressed);
    276      }
    
    private static Class defineClass(String name, ClassLoader loader, Object cookie,
    279                                       DexFile dexFile, List<Throwable> suppressed) {
    280          Class result = null;
    281          try {
    282              result = defineClassNative(name, loader, cookie, dexFile);
    283          } catch (NoClassDefFoundError e) {   
    287          } catch (ClassNotFoundException e) {       
    291          }
    292          return result;
    293      }

    defineClassNative方法是Native方法,由C/C++ 实现

    BootClassLoader和PathClassLoader的创建

    BootClassLoader是在Zygote进程的入口main方法中preload(bootTimingsTraceLog)创建的,PathClassLoader则是在Zygote进程创建SystemServer进程时创建的


    参考文章:http://liuwangshu.cn/application/classloader/1-java-classloader-.html