今天看啥  ›  专栏  ›  BlackSwift

ClassLoader的native实现

BlackSwift  · 简书  ·  · 2017-08-24 22:25

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()是这样的

  1. sun.misc.Launcher$AppClassLoader@13221655
  2. sun.misc.Launcher$ExtClassLoader@1218025c
  3. 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为例

  1. 构造filename为/String.class
  2. bootclasspath列表进行文件路径遍历,尝试获取到此文件
  3. 通过zip/fread等命令获取data作为字节流
  4. 通过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);
}

具体步骤简述如下

  1. 将SystemClassLoader配置为线程上下文
  2. 使用SystemClassLoader加载入参中的主Class
  3. 调用主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
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的工作




原文地址:访问原文地址
快照地址: 访问文章快照