记录一次 IoTDB 问题排查学到的类加载知识

什么是类加载

类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象,Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError 错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

简单来说,加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

1、通过一个类的全限定名来获取其定义的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。

类加载具体机制可以参考:

Java类加载机制

JVM 基础 - Java 类加载机制

类加载机制

双亲委派机制

双亲委派机制是指如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。类加载器层次关系如下图:

img
  • 启动类加载器(Bootstrap ClassLoader),负责加载存放在 $JAVA_HOME\jre\lib 下,或被 -Xbootclasspath 参数指定的路径中的,并且能被虚拟机识别的类库(如 rt.jar,所有的 java.* 开头的类均被 Bootstrap ClassLoader 加载)。启动类加载器是无法被 Java 程序直接引用的。
  • 扩展类加载器(Extension ClassLoader),该加载器由 sun.misc.LauncherExtClassLoader 实现,它负责加载JAVA_HOME\jre\lib\ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader),该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 自定义类加载器(User ClassLoader),如果有必要,我们还可以加入自定义的类加载器。因为 JVM自带的 ClassLoader 只能从本地文件系统加载标准的 java class 文件。

全盘负责机制

当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

缓存机制

缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区寻找该 Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区。这就是为什么修改了 Class 后,必须重启JVM,程序的修改才会生效。

ClassLoader.loadClass 源码如下,可以看到第五行注释,首先会判断该类是否被加载过。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        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
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

需要注意的是,由不同 ClassLoader 加载的具有相同****全限定****名的 Class 属于不同的 Class.

即,使用 classLoaderA 加载 org.apache.iotdb.udf.example 后,再使用 classLoaderB 加载 org.apache.iotdb.udf.example,并不会走缓存的流程,因为这被认为是不同的 Class。这一点在排查问题初期没有打日志的时候也挺让人迷惑的。

一次问题排查

问题描述

Issue 链接:[IOTDB-4899] [UDF] develop UDF class with Enum, return 500 when querying - ASF JIRA

创建 UDF 成功,但是执行时报错

img

找不到 org.apache.iotdb.udf.MySum$1 类

解决过程

org.apache.iotdb.udf.MySum$1 是什么

由于这个类的类名很像匿名类,首先查看创建 UDF 用到的 jar 包里是否有这个类,结果如下:

img

可以看到打出的 jar 包里包括了 MySum$1 这个类,但是实际项目里只有 MySum 类,所以这个类应该是编译之后自动生成的。

结合日志,在 transform 的40行才报错,这一行刚好是 swtich 代码块开始的地方,经过查证,发现 JVM 会在 swtich enum 中 case 数量大于一定值时,将这个代码块编译出一个匿名类,代码如下。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.iotdb.udf;

import org.apache.iotdb.udf.api.type.Type;

// $FF: synthetic class
class MySum$1 {
    static {
        try {
            $SwitchMap$org$apache$iotdb$udf$api$type$Type[Type.INT32.ordinal()] = 1;
        } catch (NoSuchFieldError var4) {
        }

        try {
            $SwitchMap$org$apache$iotdb$udf$api$type$Type[Type.INT64.ordinal()] = 2;
        } catch (NoSuchFieldError var3) {
        }

        try {
            $SwitchMap$org$apache$iotdb$udf$api$type$Type[Type.FLOAT.ordinal()] = 3;
        } catch (NoSuchFieldError var2) {
        }

        try {
            $SwitchMap$org$apache$iotdb$udf$api$type$Type[Type.DOUBLE.ordinal()] = 4;
        } catch (NoSuchFieldError var1) {
        }

    }
}

为什么找不到 org.apache.iotdb.udf.MySum$1

为什么找不到这个匿名类?

既然是加载这个类时出现的问题,我们首先要知道是哪个类加载器在尝试加载这个类。结合类加载机制中的全盘负责机制,我们知道这个匿名类是由加载依赖它的 org.apache.iotdb.MySum 的类加载器来加载的,我们把这个类加载器记为 A

为什么 A 加载不到 org.apache.iotdb.udf.MySum$1 ?

由于 A 成功加载了 org.apache.iotdb.udf.MySum,我们知道 A 是能成功找到 jar 包下的文件的。那么可能是 A 被关闭了,所以就无法加载到匿名类了。

排查的过程涉及 IoTDB UDF management 部分的具体代码,这里直接放出定位到的代码,由于 try-with-resource 在代码块结束后会自动调用资源的 close 方法,所以这里会自动关掉加载 org.apache.iotdb.udf.MySum 的类加载器 A,之后 org.apache.iotdb.udf.MySum$1 无法用 A 加载,于是出现了上面的报错。

try (UDFClassLoader currentActiveClassLoader =
        UDFClassLoaderManager.getInstance().updateAndGetActiveClassLoader()) {
      updateAllRegisteredClasses(currentActiveClassLoader);

      Class<?> functionClass = Class.forName(className, true, currentActiveClassLoader);
      functionClass.getDeclaredConstructor().newInstance();
      udfTable.addUDFInformation(functionName, udfInformation);
      udfTable.addFunctionAndClass(functionName, functionClass);
    } catch (IOException
        | InstantiationException
        | InvocationTargetException
        | NoSuchMethodException
        | IllegalAccessException
        | ClassNotFoundException e) {
      String errorMessage =
          String.format(
              "Failed to register UDF %s(%s), because its instance can not be constructed successfully. Exception: %s",
              functionName.toUpperCase(), className, e);
      LOGGER.warn(errorMessage, e);
      throw new UDFManagementException(errorMessage);
    }

ClassLoader.close()

在排查问题的时候,对 某 ClassLoader 加载的类在该 ClassLoader 被 close 后是否还可以被访问到存在疑问,查看源码之后发现是可以的。URLClassLoader 在被 close 后无法被用于加载新的类或资源,但是已经被 load 的类和资源仍然可以访问。

/**
 * Closes this URLClassLoader, so that it can no longer be used to load
 * new classes or resources that are defined by this loader.
 * Classes and resources defined by any of this loader's parents in the
 * delegation hierarchy are still accessible. Also, any classes or resources
 * that are already loaded, are still accessible.
 * <p>
 * In the case of jar: and file: URLs, it also closes any files
 * that were opened by it. If another thread is loading a
 * class when the {@code close} method is invoked, then the result of
 * that load is undefined.
 * <p>
 * The method makes a best effort attempt to close all opened files,
 * by catching {@link IOException}s internally. Unchecked exceptions
 * and errors are not caught. Calling close on an already closed
 * loader has no effect.
 *
 * @exception IOException if closing any file opened by this class loader
 * resulted in an IOException. Any such exceptions are caught internally.
 * If only one is caught, then it is re-thrown. If more than one exception
 * is caught, then the second and following exceptions are added
 * as suppressed exceptions of the first one caught, which is then re-thrown.
 *
 * @exception SecurityException if a security manager is set, and it denies
 *   {@link RuntimePermission}{@code ("closeClassLoader")}
 *
 * @since 1.7
 */
 public void close() throws IOException {
     SecurityManager security = System.getSecurityManager();
     if (security != null) {
         security.checkPermission(new RuntimePermission("closeClassLoader"));
     }
     List<IOException> errors = ucp.closeLoaders();

     // now close any remaining streams.

     synchronized (closeables) {
         Set<Closeable> keys = closeables.keySet();
         for (Closeable c : keys) {
             try {
                 c.close();
             } catch (IOException ioex) {
                 errors.add(ioex);
             }
         }
         closeables.clear();
     }

     if (errors.isEmpty()) {
         return;
     }

     IOException firstex = errors.remove(0);

     // Suppress any remaining exceptions

     for (IOException error: errors) {
         firstex.addSuppressed(error);
     }
     throw firstex;
 }

setContextClassLoader

setContextClassLoader 是用来打破双亲委派机制的一种手段,Java SPI 机制里就用到了它,这里不深入解释。

由于一开始遗忘了类加载的全盘负责机制,加上在 0.13 分支上看到了下面的代码块,产生了这样的误解:在加载匿名类时,会首先尝试使用线程的 ContextClassLoader(默认是 SystemClassLoader,即 AppClassLoader)来加载。

    if (!information.isBuiltin()) {
      Thread.currentThread()
        .setContextClassLoader(
            UDFClassLoaderManager.getInstance().getActiveClassLoader());
    }

由于 IoTDB 1.0 中,查询的规划线程和执行线程不一致,这段代码块是规划线程中的,所以一开始认为是没有正确的 setContextClassLoader 导致的问题,应该在执行线程中 setContextClassLoader。不过经过实验加上查阅资料,发现应该是按照全盘负责机制理解,setContextClassLoader 并不起作用,所以这段代码块应该删掉。

是否可以不关闭类加载器

由于我们用的类加载器继承自 URLClassLoader,会占用文件描述符,如果存在过多不关闭的 ClassLoader 显然是有过度占用资源的问题的。但是可能少量的 ClassLoader 并不会有大问题:

Leaving Classloader open after first use

类卸载时机

满足下述条件时一个类可能会被卸载掉:

1、该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。 2、加载该类的 ClassLoader 已经被回收。 3、该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

ClassLoader 什么时候被回收还没有找到合适的资料,欢迎补充~

请注意,ClassLoader 并不提供接口让我们显示 unload 一个类,一般来说我们只能等 JVM 自己帮我们卸载掉一个类,所以不能对卸载类有预期。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容