ClassLoader的native实现
(初稿)
最近趁着空闲,赶紧补完JVM系列。关于ClassLoader部分,主要有两个方向,一个是JVM启动时加载Rt.jar中的ClassLoader,第二个是用Java编写的应用层ClassLoader。
三种ClassLoader
众所周知,在JVM中有3种ClassLoader
|
作用 |
system |
用来加载开发者写App的代码,并可以定制ClassLoader |
extensions |
用来加载 lib/ext/*.jar等包,比较少见(本文不分析) |
bootstrap |
用原生代码来实现的,并不继承自 java.lang.ClassLoader ,用来加载rt.jar,返回是null |
我们先跑一下代码
//1. System: sun.misc.Launcher$AppClassLoader@4b67cf4d
// 下面用equals计算后,发现均相等
ClassLoader.getSystemClassLoader();
com.test.App.getClass().getClassLoader();
Class.forName("com.test.App").getClassLoader()
//2. extensions: sun.misc.Launcher$ExtClassLoader@66d3c617
//Class loaded in ext/*.jar
Class.forName("jdk.nashorn.api.scripting.AbstractJSObject")
//3. bootstrap: return null, native class loaded in rt.jar
String.class.getClassLoader()
Class.forName("java.lang.String").getClassLoader();
Class.forName("java.lang.Class").getClassLoader();
Class.forName("apple.launcher.JavaAppLauncher").getClassLoader();
ClassLoader间有继承关系
public static void main(String[] args) throws Exception{
ClassLoader classLoader = Class.forName("com.test.App").getClassLoader();
System.out.println("classLoader = " + classLoader);
while (classLoader.getParent() != null){
ClassLoader parent = classLoader.getParent();
System.out.println("parent = " + parent);
classLoader = parent;
}
}
跑完后,可以发现getParent()
是这样的
- sun.misc.Launcher$AppClassLoader@13221655
- sun.misc.Launcher$ExtClassLoader@1218025c
- null
与上面的表格描述是一致的
委托机制
通过分析源码可以发现,所谓的委托机制就是优先回源去【权威ClassLoader】查询,而不是查本地。这个很容易理解,内存中出现两个或多个String.class,最后肯定乱套。
如果是普通面试,上面的应该就够了,下面开始讲native实现
ClassLoader初始化流程
我们以嵌入式虚拟机JamVM为例
- 为简化分析,不考虑ext的ClassLoader
- 本文所写的只是某种JVM实现,与sun实现可能有不同
- 下面是经过删减与注释的代码(删去了状态判断、Lock与异常处理),并替换宏变量为字符串
本部分入参如下,我挑了个软柿子,用最简单的
jamvm -cp . Hello
接着进行断点分析从启动VM到执行Java的main函数这一过程中,涉及到Class与ClassLoader的流程。
在JVM启动开始后
1. 读取BOOTCLASSPATH
信息
伪代码如下
//eg “/jre/lib/aaa.jar:/jre/lib/bbb.jar”;
def bootpath=getProp('BOOTCLASSPATH')
List bootclasspath = bootpath.split(":")
此处的bootclasspath将在后续作为路径源,用来搜索Class
2. 反序列化Class
当需要加载某个Class时,比如String
,将进行如下操作
Class *loadSystemClass(char *classname) {
int file_len, fname_len = strlen(classname) + 8;
char buff[max_cp_element_len + fname_len];
char filename[fname_len];
Class *class = NULL;
char *data = NULL;
int i;
filename[0] = '/';
strcat(strcpy(&filename[1], classname), ".class");
// 对bootclasspath进行forEach循环
// 从file或者zip中读取byte[]流
for(i = 0; i < bcp_entries && data == NULL; i++)
if(bootclasspath[i].zip)
data = findArchiveEntry(filename + 1, bootclasspath[i].zip,
&file_len);
else
data = findFileEntry(strcat(strcpy(buff, bootclasspath[i].path),
filename), &file_len);
// 通过byte[]流反序列化为class
class = defineClass(classname, data, 0, file_len, NULL);
sysFree(data);
return class;
}
此部分的简单描述如下,以入参是String
为例
- 构造filename为
/String.class
- 对
bootclasspath
列表进行文件路径遍历,尝试获取到此文件
- 通过zip/fread等命令获取
data
作为字节流
- 通过
data
反序列化为class
3. APP的ClassLoader实现: getSystemClassLoader
了解了Boot的ClassLoader后,现在讲下用户接触的Java版ClassLoader的获取流程
// jam.c, 用户接触的ClassLoader
Object *getSystemClassLoader() {
// 从Boot级别的加锁Hash表中取出"java/lang/ClassLoader",并完成初始化
Class *class_loader = findSystemClass("java/lang/ClassLoader");
if(!exceptionOccurred()) {
MethodBlock *mb = findMethod(class_loader,
"getSystemClassLoader",
"()Ljava/lang/ClassLoader;");
if(mb != NULL) {
// 执行 java.lang.ClassLoader#getSystemClassLoader
Object *loader = *(Object**)executeStaticMethod(class_loader, mb);
if(!exceptionOccurred())
return loader;
}
}
return NULL;
}
如果你看不懂的话,下面是简化流程
- 通过BootClassLoader获取"java/lang/ClassLoader"这个Class,并进行link,
<clinit>
等操作
- 调用Java方法
java.lang.ClassLoader#getSystemClassLoader
4. 设置上下文并执行主函数
// 此步就是刚刚所分析的
if((system_loader = getSystemClassLoader()) == NULL)
goto error;
// 将SystemClassLoader配置为线程上下文
mainThreadSetContextClassLoader(system_loader);
// 使用SystemClassLoader加载入参中的主Class
main_class = findClassFromClassLoader(argv[class_arg], system_loader);
if(main_class != NULL){initClass(main_class);}
mb = lookupMethod(main_class, "main","([Ljava/lang/String;)V");
i = class_arg + 1;
if((array_class = findArrayClass("[Ljava/lang/String;") &&
(array = allocArray(array_class, argc - i, sizeof(Object*)))) {
Object **args = ARRAY_DATA(array, Object*) - i;
for(; i < argc; i++)
if(!(args[i] = Cstr2String(argv[i])))
break;
/* Call the main method */
if(i == argc)
// 调用主Class的`main`函数
executeStaticMethod(main_class, mb, array);
}
具体步骤简述如下
- 将SystemClassLoader配置为线程上下文
- 使用SystemClassLoader加载入参中的主Class
- 调用主Class的
main
函数
总结
BootClassLoader在JVM中本质就是一个反序列化器,用Groovy写就是这样的
getProp('BOOTCLASSPATH').split(':').find{dir->name in dir}.map(fread).map(parseClass)
而SystemClassLoader是纯Java的,只是在JVM中进行了初始化
附录
sourceInsight的配置
阅读C代码除了Clion外,还推荐使用sourceInsight,这个软件一般在硬件厂商用的多,虽然很丑但是速度很快,使用教程见这里
- 记得在View中打开Reference与ContextWindow
- 使用时建议关闭浏览器与各种套壳应用(否则Reference会很慢)
GroovyClassLoader
Groovy是一种基于JVM的动态语言,可以与Java代码混编,主要用于DSL快速开发。下面介绍一个Groovy中成熟的ClassLoader,看看它如何加载Class,我们先看下它的亮点
GroovyClassLoader classLoader = new GroovyClassLoader()
Class aClass = classLoader.parseClass("""
class Person{
String name;
String age;
Person(String name,String age){
System.out.println("construct Person..")
this.name = name;
this.age = age;
}
}
""")
Object o = aClass.newInstance(["suzumiya",'16'] as Object[])
println "{o.age} = ${o.age}"
是不是非常简单,可以直接在运行时通过字符串输入Class,并直接实例化。
我们先上UML图,此图由IDEA自动生成(由于简书不支持svg,这里使用了外链)
groovyclassloader
接着对parseClass
进行断点,关键位置
classLoader.parseClass()
su = unit.addSource(codeSource.getName(), codeSource.getScriptText());
//生成AST与Class
unit.compile(goalPhase);
我们可以分析一下编译的过程,这里的parser过程实际上是一个状态机构造的上下文,每处理一次,就自增一次
INITIALIZATION = 1; // Opening of files and such
PARSING = 2; // Lexing, parsing, and AST building
CONVERSION = 3; // CST to AST conversion
SEMANTIC_ANALYSIS = 4; // AST semantic analysis and elucidation
CANONICALIZATION = 5; // AST completion
INSTRUCTION_SELECTION = 6; // Class generation, phase 1
CLASS_GENERATION = 7; // Class generation, phase 2
// 8,9 在本文不涉及,故删除
此部分实现类在groovyjarjarantlr
中,其中antlr
是一种通过DSL进行parse的工具,如果你对此部分代码有兴趣,可以去看下《自制编程语言》这本书
String/File->AST->AST优化->Bytes->defineClass
我在附录提到它,主要是它用纯Java也完成了类似BootClassLoader的工作