是谁造成了 NoClassDefFoundError?
半夜睡得正香的时候,突然接到警告电话,于是翻起身就打卡电脑连上环境查看是什么情况?登录上之后发现有个微服务占用的句柄数量一直在持续上涨,最终导致了微服务内存溢出挂掉了。这个微服务在运行的过程中会建立SSH
连接,且之前这个微服务已经遇到过很多次类似的情况了,因此第一反应是哪里建立的连接又没有关闭。
猜肯定是猜不出来的,所以第一步肯定先看下日志里面哪里在报错,然后才好对症下药。打开日志之后,经过一番排查,发现日志里面有个很奇怪的报错,日志里面有打印NoClassDefFoundError
。最开始的我对这个错误的理解是不够深刻的,我的第一反应是Class
文件找不到了。于是我切换到微服务的路径下,去找这个Class
文件,发现文件是存在的。于是我又想,难道是文件的权限不对?我又用了ll
命令看了一下文件的权限,发现文件的权限也是对的。这个时候我有点懵了,心想完了,这道题不会呀,老师没教过呀!
没办法,为了保住工作,硬着头皮还是得上。俗话说,源码之下无秘密,只有根据堆栈找到对应的源代码进行分析,看看有什么怀疑点,然后又从网上搜索了一下NoClassDefFoundError
报错的含义。经过我的深思熟虑终于发现了问题的所在。
首先需要了解一下NoClassDefFoundError
的报错含义,参考why-am-i-getting-a-noclassdeffounderror-in-java这个帖子:
这段话说:NoClassDefFoundError这个报错说明之前JVM尝试过去加载这个类,但是因为某些原因失败了。现在又要使用到这个类,所以又会触发这个类的加载,但是因为之前加载这个类失败了,所以这次就不会去加载这个类了,而是直接抛出NoClassDefFoundError这个报错。
如果上面这段话不好理解,可以看下面这个例子,这个例子也是来自于上面那个帖子中的回答:
public class NoClassDefFoundError {
public static void main(String[] args) {
try {
// 这里尝试new一个对象,会触发SimpleCalculator的第一次加载
SimpleCalculator calculator1 = new SimpleCalculator();
} catch (Throwable t) {
System.out.println(t);
}
// 这里又尝试new一个对象,会触发SimpleCalculator的第二次加载
SimpleCalculator calculator2 = new SimpleCalculator();
}
}
class SimpleCalculator {
// 类加载的时候会初始化这个类变量,这里会抛出一个运行时异常
static int undefined = 1 / 0;
}
从上面的运行结果可以看到,在代码的第12行抛出了NoClassDefFoundError
的报错,这里也是第二次尝试加载这个类的地方。第一次尝试初始化SimpleCalculator
这个类时,因为初始化会初始化 undefined
这个变量,而这个变量在初始化过程中会抛出一个异常,满足了第一次报错的条件,然后第12行尝试第二次初始化这个类,因为第一次已经初始化失败了,这个时候 JVM 就直接抛出NoClassDefFoundError
这个报错,而不是尝试再次去加载这个类。
当然实际的代码不可能会写出这么明显的Bug,我出问题的代码大概是长如下这样:
public class XXXUtils {
private static final XXXBean bean = SpringContextUtils.getBean(XXXBean.class);
}
public class SpringContextUtils {
public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
}
public class XXXClazz {
public static void xxxMethod() {
XXXUtils.xxxMethod();
}
}
public class Session {
try {
XXX conn = xxx;
} finnaly {
XXXUtils.closeConn(conn);
}
}
其中的工具类 XXXUtils
实际依赖了 SpringContexUtils
来获取 Bean
,也就是依赖 Spring
上下文初始化好。但是实际在服务启动的过程中又触发了 XXXClass
的 xxxMethod
调用了 XXXUtils
的方法,这个时候就会触发 XXXUtils
的类加载,也就会触发它的 bean
变量的初始化,但是由于这个时候 Spring
上下文还没有初始化好,因此调用 SpringContextUtils.getBean()
方法就会抛出异常。在第一次初始化 XXXUtils
失败之后,等到服务正常启动,其它地方再调用 XXXUtils
的方法时,就会抛出 NoClassDefFoundError
错误,导致了 XXXUtils
的所有方法都不可用,而正常的SSH连接结束之后,会调用 XXXUtils.closeConn()
方法关闭连接,当然,因为这个时候方法不可用,所以连接也关不掉,最终导致了的句柄数量不断上涨,服务也挂掉了。