深入JVM字节码(2)

Java ClassLoader 加载过程

Posted by Jason Lee on 2020-08-26

前言

我们上一章节讨论了字节码的一些分布,具体的请看深入JVM字节码(1) Java Class 详解,字节码是对象生成的一个模板,本节将会从个源码分析来剖析一下,JVM是怎样加载类到内存的。

ClassLoader

JAVA类加载流程

Java语言系统自带有三个类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap

  • ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。

  • Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。

  • Appclass Loader 也称为SystemAppClass 加载当前应用的classpath的所有类。

JAVA 类加载顺序

  • Bootstrap CLassloder

  • Extention ClassLoader

  • AppClassLoader

    • Java类都是通过java.lang.ClassLoader的某个实例来加载的,那么到底是谁来加载java.lang.ClassLoader呢,答案就是Bootstrap Class Loader。
    • Bootstrap ClassLoader用于加载JDK内部类,如rt.jar和其他JRE中lib目录的Java类库中的类。Bootstrap ClassLoader充当所有其他ClassLoader实例的父级。
    • Bootstrap ClassLoader是JVM核心的一部分,由原生代码编写,不同的平台可能会有不同的实现

AppClassLoader 和 Extention ClassLoader

系统类加载器,是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

我们来看 ClassLoader.getSystemClassLoader的代码。
这是返回系统加载器的方法,该方法会在JVM启动早期被调用,主要是用来初始化 AppClassLoader 加载器。

并将系统类加载器放到当前线程的上下文当中。 返回是是scl

1
2
3
4
5
6
7
8
9
10
11
12
13
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
// 初始化系统加载器
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}

接下来我们看 scl 的初始化是在initSystemClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static synchronized void initSystemClassLoader() {

sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 从 launcher 中拿到class loader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
// 省略
}
}

scl 的生成过程我们要看 SystemClassLoaderAction 的run 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ClassLoader run() throws Exception {
// 可以指定系统类加载器 通过参数的方式指定
String cls = System.getProperty("java.system.class.loader");
if (cls == null) {
return parent;
}
// 创建类加载器的实例
Constructor<?> ctor = Class.forName(cls, true, parent)
.getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
ClassLoader sys = (ClassLoader) ctor.newInstance(
new Object[] { parent });
// 设置上下文
Thread.currentThread().setContextClassLoader(sys);
return sys;
}

这里会不会有个疑问,我们所说的 AppClassLoader在哪里出现的呢?别着急,我们接下来来看 Launcher ,因为ClassLoader 就是从这里出现的。

通过上面的分析,我们知道, 系统类的加载器是从 Launcher 中拿到的,接下来我们来看sun.misc.Launcher,它是一个java虚拟机的入口应用。 但是在我们OpenJDK home 目录下 src.zip 源码包中是不全的,因此我们需要看完整的JDK实现。

这里推荐几个扩展阅读:

我们接下来主要看一下 sun.misc.Launcher 的实现。原本笔者是想打断开来看一下加载过程的,但是断点失败了,所有去查了一下相关资料:

OpenJDK 对于 Launcher 类的描述:This class is used by the system to launch the main application.system 。

于是我又翻了翻 IBM 关于 Java 中 Debug 实现原理的介绍,文章地址如下:https://www.ibm.com/developerworks/cn/java/j-lo-jpda1/
文章中说到:JDI(Java Debug Interface)是三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。参考 Oracle 的官方文档:https://docs.oracle.com/javase/9/docs/api/jdk.jdi-summary.html
可以知道 jdi 是一个位于 tools.jar 包下的子包,而 tools.jar 也是由 BootStrap 类加载器负责加载的。
所以现在我们可以知道了,为 Java 提供 Debug 支持的类加载和 Launcher 的类加载都是由 Bootstrap 类加载器负责的,只是后者先发生,所以 debug 功能实现的时候,Launcher 的构造器早已运行结束了。

Launcher

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
public class Launcher {

private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
// 单例模式
public static Launcher getLauncher() {
return launcher;
}

private ClassLoader loader;

public Launcher() {
// Create the extension class loader
ClassLoader extcl;
// 省略其他
extcl = ExtClassLoader.getExtClassLoader();

// 省略其他

loader = AppClassLoader.getAppClassLoader(extcl);

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);

// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
// ...
}

/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
  • Launcher 是整个应用程序的入口启动类,主要是初始化了ExtClassLoader和AppClassLoader。

首先我们来看 ExtClassLoader 的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static ExtClassLoader getExtClassLoader() throws IOException {
// 获取 java.ext.dirs
final File[] dirs = getExtDirs();
// 省略 try catch
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
// 目录注册 相当于缓存
MetaIndex.registerDirectory(dirs[i]);
}
// 创建 ExtClassLoader
return new ExtClassLoader(dirs);
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
// 创建 AppClassLoader extcl 为他的父类
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}

总结

上文提到的 Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,

JVM启动时通过 Bootstrap 类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。

然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoaderAppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。

双亲委托机制

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

这种双亲委派模式的好处,一个可以避免类的重复加载,另外也避免了java的核心API被篡改。

  • 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
    递归,重复第1部的操作。

  • 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。

  • Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。

  • ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

我们还需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

类加载过程机器双亲委托实现原理

我们先看这几个方法的层级

loadClass(String)

AppClassLoader 的loadClass 直接调用的是 ClassLoader 的loadClass 方法

该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先从缓存查找该class对象,找到就不用重新加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {//是否需要在加载时进行解析
resolveClass(c);
}
return c;
}
}
  • findLoadedClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
//简单的检查加载的类的名字是否为空或是无效
private boolean checkName(String name) {
if ((name == null) || (name.length() == 0))
return true;
if ((name.indexOf('/') != -1)
|| (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
return false;
return true;
}
private native final Class findLoadedClass0(String name);

我们发现findLoadedClass0是一个native方法。查了下openjdk源码如下:

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
JNIEXPORT jclass JNICALL
Java_java_lang_ClassLoader_findLoadedClass0(JNIEnv *env, jobject loader,
jstring name)
{
if (name == NULL) {
return 0;
} else {
return JVM_FindLoadedClass(env, loader, name);
}
}

// JVM_FindLoadedClass 的实现
JVM_ENTRY(jclass, JVM_FindLoadedClass(JNIEnv *env, jobject loader, jstring name))
JVMWrapper("JVM_FindLoadedClass");
ResourceMark rm(THREAD);

Handle h_name (THREAD, JNIHandles::resolve_non_null(name));
Handle string = java_lang_String::internalize_classname(h_name, CHECK_NULL);

const char* str = java_lang_String::as_utf8_string(string());
// Sanity check, don't expect null
if (str == NULL) return NULL;

const int str_len = (int)strlen(str);
if (str_len > Symbol::max_length()) {
//类名长度有限制
// It's impossible to create this class; the name cannot fit
// into the constant pool.
return NULL;
}
// 查看全局变量表的名字
TempNewSymbol klass_name = SymbolTable::new_symbol(str, str_len, CHECK_NULL);

// Security Note:
// The Java level wrapper will perform the necessary security check allowing
// us to pass the NULL as the initiating class loader.
Handle h_loader(THREAD, JNIHandles::resolve(loader));
if (UsePerfData) {
is_lock_held_by_thread(h_loader,
ClassLoader::sync_JVMFindLoadedClassLockFreeCounter(),
THREAD);
}
//全局符号字典查找该类的符号是否存在
klassOop k = SystemDictionary::find_instance_or_array_klass(klass_name,
h_loader,
Handle(),
CHECK_NULL);

return (k == NULL) ? NULL :
(jclass) JNIHandles::make_local(env, Klass::cast(k)->java_mirror());
JVM_END
}

这段看不懂没关系,我会在JVM的启动的时候,再次分析,这里只需要知道是JVM负责查找Class就好了。

接着的逻辑就是类的双亲加载机制的实现。把类加载的任务交给父类加载器执行,直到父类加载器为空,此时会返回通过JDK提供的系统启动类加载器加载的类。
我们发现 findBootstrapClassOrNull 也是个native方法

1
2
3
4
5
6
7
8
9
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;

return findBootstrapClass(name);
}

// return null if not found
private native Class<?> findBootstrapClass(String name);

由于篇幅有限,我们这里就不在介绍 findBootstrapClass 的jdk 源码了。

findClass

该方法和它的名字一样,就是根据类的名字ClassLoader不提供该方法的具体实现,要求我们根据自己的需要来覆写该方法。
所以我们可以看一看URLClassLoader对findClass方法的实现,类加载的工作又被代理给了defineClass方法:

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
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}

return result;
}
  • defineClass
    defineClass方法主要是把字节数组转化为类的实例。同时definClass方法为final的,故不可以覆写。
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
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
URL url = res.getCodeSourceURL();
if (i != -1) {
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// Now read the class bytes and define the class
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
}
}

最后几个defineClass 会落在几个Natice方法上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

private native Class<?> defineClass0(
String name, byte[] b,
int off, int len,
ProtectionDomain pd);

private native Class<?> defineClass1(
String name, byte[] b,
int off, int len,
ProtectionDomain pd, String source);

private native Class<?> defineClass2(
String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);

这里不做分析了。

总结

到此我们就分析完了整个ClassLoader的加载过程,其中有很多细节我们了解,我们下一章再去分析,下一章节,我们主要针对于JVM的源码分析一下JVM启动过程。

参考



支付宝打赏 微信打赏

赞赏一下