排错: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。