前言

上一篇我们了解了字节码的结构,这些字节码文件都是什么时候被加载到内存中的,今天就来聊聊类加载。

类的加载时机

众所周知,电脑的内存是有限的,在使用的过程有些类没有被使用就不应该把它加载到内存中,加载了不用的类,会导致可用的内存越来越少。

Java 中会在下面这两种情况下把 .class 文件加载到内存中

  1. 调用类的构造方法
  2. 调用类的静态变量或静态方法

Java 类加载器

Java 中有 AppClassLoaderExtClassLoaderBootstrapClassLoader ,我们依次看下

AppClassLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static class AppClassLoader extends URLClassLoader {

    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        final String var1 = System.getProperty("java.class.path");
        final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
            public Launcher.AppClassLoader run() {
                URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                return new Launcher.AppClassLoader(var1x, var0);
            }
        });
    }
}

从上面的代码中可以发现, AppClassLoader 会加载 java.class.path 路径下的文件。也就是 CLASS_PATH 环境变量的值。

我们自己写的代码以及第三方库都是由 AppClassLoader 加载的。

通过如下代码查看 java.class.path 的具体路径

1
System.out.println(System.getProperty("java.class.path"));

最后只会输出一个 . 也就是当前文件路径。

ExtClassLoader

ExtClassLoader 在 1.9 之后改名为 PlatformClassLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static class ExtClassLoader extends URLClassLoader {
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
        final File[] var0 = getExtDirs();

        try {
            return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                public Launcher.ExtClassLoader run() throws IOException {
                    int var1 = var0.length;

                    for(int var2 = 0; var2 < var1; ++var2) {
                        MetaIndex.registerDirectory(var0[var2]);
                    }

                    return new Launcher.ExtClassLoader(var0);
                }
            });
        } catch (PrivilegedActionException var2) {
            throw (IOException)var2.getException();
        }
    }

    private static File[] getExtDirs() {
        String var0 = System.getProperty("java.ext.dirs");
        File[] var1;
        if (var0 != null) {
            StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
            int var3 = var2.countTokens();
            var1 = new File[var3];

            for(int var4 = 0; var4 < var3; ++var4) {
                var1[var4] = new File(var2.nextToken());
            }
        } else {
            var1 = new File[0];
        }

        return var1;
    }
}

ExtClassLoaderAppClassLoader 类似,不同的地方是加载的路径不一样。 ExtClassLoader 加载的是 java.ext.dirs 环境变量的值。 同样我们原来查看一下 java.ext.dirs 的路径

1
System.out.println(System.getProperty("java.ext.dirs"));

输出如下内容

1
2
3
4
5
6
/Users/test/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java

BootstrapClassLoader

BootstrapClassLoaderExtClassLoaderAppClassLoader 不一样,它是 C/C++ 编写的,是虚拟机的一部分。

BootstrapClassLoader 加载的是 sun.boot.class.path 下的文件。 同样我们原来查看一下 sun.boot.class.path 的路径

1
System.out.println(System.getProperty("sun.boot.class.path"));

输出如下内容

1
2
3
4
5
6
7
8
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/resources.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/rt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/sunrsasign.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/jsse.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/jce.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/jfr.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/classes

双亲委派

经过上一小节的学习,我们知道在 Java 中有三种类加载器,这三个类加载器加载的都是不同路径下的。那么在具体加载类的时候是怎么决定用哪个类加载器加载的呢?

Java 中通过 双亲委派机制 来加载类的。如下所示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
            }
        } 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;
}

loadClass 中可以看出,先判断该类是否已经被加载了,如果加载了就直接返回。

如果该类没有加载,就会看看有没有 parent ,如果有就委托给 parent ,最后 parent 中没有找到该类,则调用自己的 findClass 去加载。

如果 parentnull 则会委托给 BootstrapClassLoader 去加载,如果在 BootstrapClassLoader 中没有加载成功,则也是调用自己的 findClass 去加载。

AppClassLoader 中的 parent

我们来看看 AppClassLoader 中的 parent 是怎么来的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        // 省略代码

}


public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    final String var1 = System.getProperty("java.class.path");
    final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
    return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
        public Launcher.AppClassLoader run() {
            URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
            return new Launcher.AppClassLoader(var1x, var0);
        }
    });
}


AppClassLoader(URL[] var1, ClassLoader var2) {
    super(var1, var2, Launcher.factory);
    this.ucp.initLookupCache(this);
}

// URLClassLoader.java
public URLClassLoader(URL[] urls, ClassLoader parent,
                      URLStreamHandlerFactory factory) {
    super(parent);
    // 省略代码
}

// SecureClassLoader.java
protected SecureClassLoader(ClassLoader parent) {
    super(parent);
    // 省略代码
}


// ClassLoader.java
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    // 省略代码
}

一路跟踪发现 parentClassLoader 中的一个成员变量,是在构造方法中传入的,在 AppClassLoader 的构造方法中传入一个 ClassLoader ,经过层层传递最后赋值给了 parent

这个 parent 的来源是在 getAppClassLoader 传进去的,而这个 ClassLoader 就是 Launcher.ExtClassLoader.getExtClassLoader(); 也就是 AppClassLoader 中的 parent 就是 ExtClassLoader

ExtClassLoader 中的 parent

1
2
3
4
public ExtClassLoader(File[] var1) throws IOException {
    super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
    SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}

ExtClassLoader 中的 parent 赋值为 null 了。

这也就解释了为什么为什么叫 双亲委派 了。也就是 AppClassLoader 会先委派给 ExtClassLoader ,而 ExtClassLoader 会委派给 BootstrapClassLoader ,如果委托方都没能成功加载,那就自己去加载。

Android 中的 ClassLoader

Android 中的 ClassLoaderJVM 类似,也是双亲委派机制。但是还是有些不同的,比如: JVM 加载的是 .class 文件,而 Android 中的 .class 文件会通过 dx 工具转换成 dex 。所以 Android 中加载的是 .dex 文件。

Android 中用来加载的 ClassLoaderPathClassLoaderDexClassLoader

PathClassLoader

PathClassLoader 一般用来加载系统 APK 和已经被安装到手机上的 APK 中的 dex 文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath,ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }

    @libcore.api.CorePlatformApi
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders) {
        super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
    }
}

其中 dexPath 就是加载的路径,可以包含多个,使用 : 分隔。 librarySearchPath 是 C/C++ native 库的路径

DexClassLoader

DexClassLoader 可以加载特定路径下的 dex 文件,这就给热修复创造了条件。

1
2
3
4
5
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

dexPathPathClassLoader 中一样,也是加载路径,可以包含多个,用 : 分隔。

optimizedDirectory 用来缓存优化后的 dex 路径,这个从 API 26 就被废弃了。

总结

Java 中有三种类加载器,分别是 BootstrapClassLoaderExtClassLoaderAppClassLoader 。他们分别用来加载 sun.boot.class.pathjava.ext.dirsjava.class.path 下的文件。

Android 中有两种类加载器,分别是 PathClassLoaderDexClassLoaderPathClassLoader 用来加载已经安装到手机上的 APKDexClassLoader 可以加载任意位置的 dex 文件,是热修复的基础。

类加载器通过 双亲委派 机制来决定使用哪个类加载器。

参考