排错:Java 的 ClassNotFoundException 异常

Table of Contents

ClassNotFoundException 异常是我们在日常开发中常遇错误之一。

Java 库将二进制 class 文件集于 .jar 文件中,JVM 运行时,CLASSPATH 决定了如何寻找 .jar 文件。所以理解 CLASSPATH 和 JAR 是关键。

1. CLASSPATH

JVM 如何找到 JAR,在启动时就已定好。请看 HotSpot 源码相关实现:

// 文件:hotspot/src/share/tools/launcher/java.c

if ((s = getenv("CLASSPATH")) == 0) {
  s = ".";
 }
#ifndef JAVA_ARGS
SetClassPath(s); // 感兴趣的请阅读此函数的实现
#endif

2. JAR

实际上 JAR 只是一个压缩文件,可通过 file 命令查看:

$ file clojure-1.8.0.jar
clojure-1.8.0.jar: Zip archive data, at least v1.0 to extract

Java 代码编译成 class 文件后,打包在 JAR 文件中。JVM 启动时根据 CLASSPATH 决定自身能找到哪些 JAR 文件。调用一个类时,就依赖它们。

jar 加载流程(hotspot/src/share/tools/launcher/java.c):

运行 java 命令时,干活的是 JavaMain 函数:

1、调用 InitializeJVM 初始化虚拟机环境

2、如果指定了 .jar 文件,就调用 GetMainClassName 获得主类

3、如果是指定的类,就调用 LoadClass 加载类

4、调用 (*env)->GetStaticMethodID 获得主类的 ID

5、调用 main 方法:

(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

这里 mainArgs 会从命令行中取参数,并且转换成 Java 内部的 Array 对象:

mainArgs = NewPlatformStringArray(env, argv, argc);

3. 类是如何调用的

static jclass
LoadClass(JNIEnv *env, char *name)
{
  char *buf = JLI_MemAlloc(strlen(name) + 1);
  char *s = buf, *t = name, c;
  jclass cls;
  jlong start, end;

  if (_launcher_debug)
    start = CounterGet();

  // 把“.”转换成“/”
  do {
    c = *t++;
    *s++ = (c == '.') ? '/' : c;
  } while (c != '\0');
  // 再根据转换后的路径寻找类
  cls = (*env)->FindClass(env, buf);
  JLI_MemFree(buf);

  if (_launcher_debug) {
    end   = CounterGet();
    printf("%ld micro seconds to load main class\n",
           (long)(jint)Counter2Micros(end-start));
    printf("----_JAVA_LAUNCHER_DEBUG----\n");
  }

  return cls;
}

大致:假如要寻找“org.shellcodes.Hello”这个类,将其名替换成“org/shellcodes/Hello”,如果没有找到文件 org/shellcodes/Hello.class,就触发 ClassNotFoundException 异常。

4. 当出现 ClassNotFoundException

检查 CLASSPATH。如果是在打包后运行遇上,就用“jar tvf filename.jar”命令,看是否有相关的 class 文件。比如找不到 org.shellcodes.Hello,就看压缩包中是否有 org/shellcodes/Hello.class。