class文件

我们在进行Java开发的时候,编译出来的源文件是 .java 文件,但是 .java 文件并不能直接运行,这些文件经过Java编译器编译成 .class 文件之后,就可以在jvm之中运行了,这也是为什么java支持多平台的原因。这个 .java 文件变成 .class 文件的过程就叫做类加载,需要用到的就是 ClassLoader, 即类加载器。

类加载的流程

类加载器ClassLoader - 图1

  • 加载:第一步,通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

  • 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证

  • 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中

  • 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)

  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

这便是类加载的5个过程,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。

jvm提供了三个类加载器:

  • Bootstrap ClassLoader 启动类加载器:最顶层的加载类,主要加载核心类库,使用C++语言实现的,是虚拟机自身的一部分。它讲 %JAVA_HOME%/lib 路径下的核心类库或 -Xbootclasspath 参数指定的路径下的jar包加载到内存中。(虚拟机是按照文件名识别加载jar包的,如果文件名不被识别,即使把jar包丢到lib目录下也无作用。此外出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
  • Extension ClassLoader 扩展类加载器:扩展的类加载器,加载目录 %JAVA_HOME%\lib\ext 目录下的jar包和class文件。还可以加载 -D java.ext.dirs 选项指定的目录
  • App ClassLoader/SystemAppClass 系统类加载器:负责加载系统类路径 java -classpath-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器。

加载顺序为:

  1. Bootstrap ClassLoader
  2. Extention ClassLoader
  3. AppClassLoader
  1. public class Launcher {
  2. private static URLStreamHandlerFactory factory = new Launcher.Factory();
  3. private static Launcher launcher = new Launcher();
  4. private static String bootClassPath = System.getProperty("sun.boot.class.path");
  5. private ClassLoader loader;
  6. private static URLStreamHandler fileHandler;
  7. public static Launcher getLauncher() {
  8. return launcher;
  9. }
  10. public Launcher() {
  11. Launcher.ExtClassLoader var1;
  12. try {
  13. var1 = Launcher.ExtClassLoader.getExtClassLoader();
  14. } catch (IOException var10) {
  15. throw new InternalError("Could not create extension class loader", var10);
  16. }
  17. try {
  18. this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
  19. } catch (IOException var9) {
  20. throw new InternalError("Could not create application class loader", var9);
  21. }
  22. }
  23. public ClassLoader getClassLoader() {
  24. return this.loader;
  25. }
  26. static class AppClassLoader extends URLClassLoader{}
  27. static class ExtClassLoader extends URLClassLoader{}

sum.misc.Launcher,它是一个jvm的入口应用

从源码我们可以看到

  1. Launcher中通过 System.getProperty(“sun.boot.class.path”) 得到了字符串 bootClassPath,这个是BootstrapClassLoader 加载的jar包路径
  2. Launcher初始化了 ExtCLassLoader 和 AppClassLoader

再看看 ExtClassLoader 源码

  1. static class ExtClassLoader extends URLClassLoader {
  2. public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
  3. final File[] var0 = getExtDirs();
  4. try {
  5. return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
  6. public Launcher.ExtClassLoader run() throws IOException {
  7. int var1 = var0.length;
  8. for(int var2 = 0; var2 < var1; ++var2) {
  9. MetaIndex.registerDirectory(var0[var2]);
  10. }
  11. return new Launcher.ExtClassLoader(var0);
  12. }
  13. });
  14. } catch (PrivilegedActionException var2) {
  15. throw (IOException)var2.getException();
  16. }
  17. }
  18. void addExtURL(URL var1) {
  19. super.addURL(var1);
  20. }
  21. public ExtClassLoader(File[] var1) throws IOException {
  22. super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
  23. SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
  24. }
  25. private static File[] getExtDirs() {
  26. String var0 = System.getProperty("java.ext.dirs");
  27. File[] var1;
  28. if (var0 != null) {
  29. StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
  30. int var3 = var2.countTokens();
  31. var1 = new File[var3];
  32. for(int var4 = 0; var4 < var3; ++var4) {
  33. var1[var4] = new File(var2.nextToken());
  34. }
  35. } else {
  36. var1 = new File[0];
  37. }
  38. return var1;
  39. }
  40. }

可以看到之前说的,可以通过指定 -D java.ext.dirs 参数来添加和改变ExtClassLoader的加载路径。

AppClassLoader 源码

  1. static class AppClassLoader extends URLClassLoader {
  2. final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
  3. public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
  4. final String var1 = System.getProperty("java.class.path");
  5. final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
  6. return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
  7. public Launcher.AppClassLoader run() {
  8. URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
  9. return new Launcher.AppClassLoader(var1x, var0);
  10. }
  11. });
  12. }
  13. AppClassLoader(URL[] var1, ClassLoader var2) {
  14. super(var1, var2, Launcher.factory);
  15. this.ucp.initLookupCache(this);
  16. }
  17. }

可以看到AppClassLoader加载的就是 java.class.path 下的路径。

父加载器

这里先来看段代码

  1. @SpringBootTest
  2. public class test {
  3. @Test
  4. public void test(){
  5. System.out.println(test.class.getClassLoader().toString());
  6. System.out.println(int.class.getClassLoader().toString());
  7. }
  8. }

这里我们获取该 test 类的加载器,以及一些基础类型的如 int 的加载器并输出,结果是:

image.png

上图我们可以看到,我们创建的类是由 AppClassLoader 加载器加载的,和上面所说的没错,但是在获取 int 的时候却抛空指针异常了,意思是 int.class 这类基础类没有类加载器吗?

答案是否,int.class 是由 BootstrapClassLoader 加载的。为了解决这个问题,首先我们先知道一个知识点:
每个类加载器都有一个父加载器

再看看下面这段代码

  1. @SpringBootTest
  2. public class test {
  3. @Test
  4. public void test(){
  5. System.out.println(test.class.getClassLoader().toString());
  6. System.out.println(test.class.getClassLoader().getParent().toString());
  7. System.out.println(test.class.getClassLoader().getParent().getParent().toString());
  8. }
  9. }

结果是:

image.png

由此我们可以看见,AppClassLoader 是由 ExtClassLoader 加载的,但是 ExtClassLoader 的父加载器又双抛NullPointer异常了。

这里我们需要注意一个点,父加载器不是父类

在之前的代码里,我们知道了 ExtClassLoader 和 AppClassLoader 都是继承于 URLClassLoader 的,但是为什么 AppClassLoader 的 getParent() 会获得 ExtClassLoader 实例呢?


类加载器ClassLoader - 图4

原因是在 URLClassLoader 中没有 getParent() 方法,该方法在 ClassLoader 中

  1. public abstract class ClassLoader {
  2. // The parent class loader for delegation
  3. // Note: VM hardcoded the offset of this field, thus all new fields
  4. // must be added *after* it.
  5. private final ClassLoader parent;
  6. private ClassLoader(Void unused, ClassLoader parent) {
  7. this.parent = parent;
  8. if (ParallelLoaders.isRegistered(this.getClass())) {
  9. parallelLockMap = new ConcurrentHashMap<>();
  10. package2certs = new ConcurrentHashMap<>();
  11. domains =
  12. Collections.synchronizedSet(new HashSet<ProtectionDomain>());
  13. assertionLock = new Object();
  14. } else {
  15. // no finer-grained lock; lock on the classloader instance
  16. parallelLockMap = null;
  17. package2certs = new Hashtable<>();
  18. domains = new HashSet<>();
  19. assertionLock = this;
  20. }
  21. }
  22. protected ClassLoader(ClassLoader parent) {
  23. this(checkCreateClassLoader(), parent);
  24. }
  25. protected ClassLoader() {
  26. this(checkCreateClassLoader(), getSystemClassLoader());
  27. }
  28. @CallerSensitive
  29. public final ClassLoader getParent() {
  30. if (parent == null)
  31. return null;
  32. SecurityManager sm = System.getSecurityManager();
  33. if (sm != null) {
  34. // Check access to the parent class loader
  35. // If the caller's class loader is same as this class loader,
  36. // permission check is performed.
  37. checkClassLoaderPermission(parent, Reflection.getCallerClass());
  38. }
  39. return parent;
  40. }
  41. @CallerSensitive
  42. public static ClassLoader getSystemClassLoader() {
  43. initSystemClassLoader();
  44. if (scl == null) {
  45. return null;
  46. }
  47. SecurityManager sm = System.getSecurityManager();
  48. if (sm != null) {
  49. checkClassLoaderPermission(scl, Reflection.getCallerClass());
  50. }
  51. return scl;
  52. }
  53. private static synchronized void initSystemClassLoader() {
  54. if (!sclSet) {
  55. if (scl != null)
  56. throw new IllegalStateException("recursive invocation");
  57. sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
  58. if (l != null) {
  59. Throwable oops = null;
  60. scl = l.getClassLoader();
  61. try {
  62. scl = AccessController.doPrivileged(
  63. new SystemClassLoaderAction(scl));
  64. } catch (PrivilegedActionException pae) {
  65. oops = pae.getCause();
  66. if (oops instanceof InvocationTargetException) {
  67. oops = oops.getCause();
  68. }
  69. }
  70. if (oops != null) {
  71. if (oops instanceof Error) {
  72. throw (Error) oops;
  73. } else {
  74. // wrap the exception
  75. throw new Error(oops);
  76. }
  77. }
  78. }
  79. sclSet = true;
  80. }
  81. }
  82. }

getParent() 方法实际上返回一个 ClassLoader 对象的 parent, parent 的赋值是在 ClassLoader 对象的构造方法中,它有两个情况:

  1. 由外部类创建ClassLoader时直接指定一个ClassLoader为parent
  2. 由 getSystemClassLoader() 方法生成,也就是在 sun.misc.Laucher 通过 getClassLoader() 获取,也就是AppClassLoader。直白的说,一个 ClassLoader 创建时如果没有指定 parent,那么它的 parent 默认就是AppClassLoader

在先前的 Launcher 代码中,有以下三句代码,说明了问题AppClassLoader的parent是一个ExtClassLoader实例

  1. ClassLoader extcl;
  2. extcl = ExtClassLoader.getExtClassLoader();
  3. loader = AppClassLoader.getAppClassLoader(extcl);

而 ExtClassLoader 并没有直接找到对 parent 的赋值,它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数

  1. public ExtClassLoader(File[] var1) throws IOException {
  2. super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
  3. SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
  4. }
  1. public URLClassLoader(URL[] urls, ClassLoader parent,
  2. URLStreamHandlerFactory factory) {
  3. super(parent);
  4. // this is to make the stack depth consistent with 1.1
  5. SecurityManager security = System.getSecurityManager();
  6. if (security != null) {
  7. security.checkCreateClassLoader();
  8. }
  9. acc = AccessController.getContext();
  10. ucp = new URLClassPath(urls, factory, acc);
  11. }


因此,ExtClassLoader 的 parent 为 null的问题就解决了。

然后又来了下一个问题,ExtClassLoader的父加载器为null,但是Bootstrap CLassLoader却可以当成它的父加载器这又是为何呢?

双亲委托

BootstrapClassLoader 是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过 Bootstrap 类加载器加载 rt.jar 等核心jar包中的class文件,之前的int.class,String.class 都是由它加载。然后呢,我们前面已经分析了,JVM初始化 sun.misc.Launcher 并创建 ExtensionClassLoader 和 AppClassLoader 实例。并将 ExtClassLoader 设置为 AppClassLoader 的父加载器。Bootstrap 没有父加载器,但是它却可以作用一个 ClassLoader 的父加载器。比如 ExtClassLoader。这也可以解释之前通过 ExtClassLoader 的 getParent 方法获取为Null的现象。

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到 BootstrapClassLoader,如果 BootstrapClassloader 找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托

类加载器ClassLoader - 图5

以上图举例:

  1. 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器
  2. 递归,重复第1步的操作
  3. 如果 ExtClassLoader 也没有加载过,则由 BootstrapClassLoader 出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是 sun.mic.boot.class 下面的路径。找到就返回,没有找到,让子加载器自己去找
  4. BootstrapClassLoader 如果没有查找成功,则 ExtClassLoader 自己在 java.ext.dirs 路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找
  5. ExtClassLoader 查找不成功,AppClassLoader 就自己查找,在 java.class.path 路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常

总结:委托是从下向上,然后具体查找过程却是自上至下

重要方法

loadClass()

通过指定的全限定类名加载class,它通过同名的 loadClass(String name, boolean resole) 方法。

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. c = parent.loadClass(name, false);
  12. } else {
  13. c = findBootstrapClassOrNull(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. // ClassNotFoundException thrown if class not found
  17. // from the non-null parent class loader
  18. }
  19. if (c == null) {
  20. // If still not found, then invoke findClass in order
  21. // to find the class.
  22. long t1 = System.nanoTime();
  23. c = findClass(name);
  24. // this is the defining class loader; record the stats
  25. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  26. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  27. sun.misc.PerfCounter.getFindClasses().increment();
  28. }
  29. }
  30. if (resolve) {
  31. resolveClass(c);
  32. }
  33. return c;
  34. }
  35. }

从以上方法可以看到其步骤是:

  1. 执行 findLoadedClass(String) 去检测这个class是否已经加载过了
  2. 如果没有加载过,执行父加载器的 loadClass 方法。如果父加载器为null,则jvm内置的加载器 BootstrapClassLoader 去替代。
  3. 如果向上委托父加载器没有加载成功,则通过 findClass(String) 查找

如果在上面的步骤中找到了 class,且 resolve 为 true,则调用 resolveClass(Class) 方法生成最终的Class对象。

另外,要注意的是如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。

自定义ClassLoader

如果在某种情况下,我们需要动态加载一些东西,比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载的时候,需要我们自定义一个classloader。

步骤:

  1. 编写一个类继承自ClassLoader抽象类。
  2. 复写它的 findClass() 方法
  3. 在 findClass() 方法中调用 defineClass()

defineClass() 这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常

一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件.

假设我们需要一个自定义的classloader,默认加载路径为D:\lib下的jar包和资源。
我们写编写一个测试用的类文件,Test.java

  1. package com.frank.test;
  2. public class Test {
  3. public void say(){
  4. System.out.println("Say Hello");
  5. }
  6. }

然后将它编译过的class文件Test.class放到D:\lib这个路径下

  1. import java.io.ByteArrayOutputStream;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.FileNotFoundException;
  5. import java.io.IOException;
  6. public class DiskClassLoader extends ClassLoader {
  7. private String mLibPath;
  8. public DiskClassLoader(String path) {
  9. // TODO Auto-generated constructor stub
  10. mLibPath = path;
  11. }
  12. @Override
  13. protected Class<?> findClass(String name) throws ClassNotFoundException {
  14. // TODO Auto-generated method stub
  15. String fileName = getFileName(name);
  16. File file = new File(mLibPath,fileName);
  17. try {
  18. FileInputStream is = new FileInputStream(file);
  19. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  20. int len = 0;
  21. try {
  22. while ((len = is.read()) != -1) {
  23. bos.write(len);
  24. }
  25. } catch (IOException e) {
  26. e.printStackTrace();
  27. }
  28. byte[] data = bos.toByteArray();
  29. is.close();
  30. bos.close();
  31. return defineClass(name,data,0,data.length);
  32. } catch (IOException e) {
  33. // TODO Auto-generated catch block
  34. e.printStackTrace();
  35. }
  36. return super.findClass(name);
  37. }
  38. //获取要加载 的class文件名
  39. private String getFileName(String name) {
  40. // TODO Auto-generated method stub
  41. int index = name.lastIndexOf('.');
  42. if(index == -1){
  43. return name+".class";
  44. }else{
  45. return name.substring(index+1)+".class";
  46. }
  47. }
  48. }

测试

  1. import java.lang.reflect.InvocationTargetException;
  2. import java.lang.reflect.Method;
  3. public class ClassLoaderTest {
  4. public static void main(String[] args) {
  5. // TODO Auto-generated method stub
  6. //创建自定义classloader对象。
  7. DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
  8. try {
  9. //加载class文件
  10. Class c = diskLoader.loadClass("com.frank.test.Test");
  11. if(c != null){
  12. try {
  13. Object obj = c.newInstance();
  14. Method method = c.getDeclaredMethod("say",null);
  15. //通过反射调用Test类的say方法
  16. method.invoke(obj, null);
  17. } catch (InstantiationException | IllegalAccessException
  18. | NoSuchMethodException
  19. | SecurityException |
  20. IllegalArgumentException |
  21. InvocationTargetException e) {
  22. // TODO Auto-generated catch block
  23. e.printStackTrace();
  24. }
  25. }
  26. } catch (ClassNotFoundException e) {
  27. // TODO Auto-generated catch block
  28. e.printStackTrace();
  29. }
  30. }
  31. }