在长时间运行的进程中,什么可能导致突然的ClassNotFoundException?

我们有一个非常小的Web服务(少于1K行代码),由Jetty运行。即使在我们的压力测试阶段,该服务也始终运行良好。但是,经过13天的正常运行时间,我们在同一天在两个节点中遇到了ClassNotFoundException。

奇怪的是,没有找到的类已经存在(它是启动例程的一部分,它不断用于处理以前的请求)。实际上,只需重新启动该过程即可解决问题。两个节点都位于不同的计算机中,并且彼此独立。它们不依赖于外部资源,除了一个 JMS 连接。

在谷歌搜索时,我找不到相关信息,因为大多数报告的问题都与启动Java进程时类路径中缺少类有关,而我们的情况并非如此。我们怀疑可能存在以某种方式损坏JVM内存的内存泄漏,但这不能解释为什么相同的问题同时发生在两个节点中。在过去的五天里,我们一直在运行密集的压力测试,连接了一个JVM监视器和一个内存泄漏分析器,一切似乎都很好。对于此测试,我们将进程内存从 2GB 减少到 512MB。

详:

  • 使用 Java HotSpot(TM) 64 位服务器虚拟机(内部版本 16.3-b01,混合模式)
  • 使用码头运行器-8.1.0.RC5.jar
  • 原cmd行:java-Xmx2048M-jar jetty-runner-8.1.0.RC5.jar--port 5000 webapp.war
  • 英特尔至强 E5-2680 8 核 (x2) + 16GB 内存
  • Red Hat Enterprise Linux 6
  • 一些正在使用的框架:JBoss Resteasy,Spring IoC,Guava。

您能否就什么会使JVM突然“忘记”以前加载的类的存在而无法再次加载它提供一些想法?

Caused by: java.lang.ClassNotFoundException: com.a.b.c.SomeClass
    at java.net.URLClassLoader$1.run(URLClassLoader.java:202) ~[na:1.6.0_37]
    at java.security.AccessController.doPrivileged(Native Method) ~[na:1.6.0_37]
    at java.net.URLClassLoader.findClass(URLClassLoader.java:190) ~[na:1.6.0_37]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:306) ~[na:1.6.0_37]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) ~[na:1.6.0_37]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:247) ~[na:1.6.0_37]
    at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:424) ~[na:na]
    at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:377) ~[na:na]
    at java.lang.Class.forName0(Native Method) ~[na:1.6.0_37]
    at java.lang.Class.forName(Class.java:247) ~[na:1.6.0_37]
    at sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:95) ~[na:1.6.0_37]
    at sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:107) ~[na:1.6.0_37]
    at sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:31) ~[na:1.6.0_37]
    at sun.reflect.annotation.AnnotationParser.parseSig(AnnotationParser.java:370) ~[na:1.6.0_37]
    at sun.reflect.annotation.AnnotationParser.parseClassValue(AnnotationParser.java:351) ~[na:1.6.0_37]
    at sun.reflect.annotation.AnnotationParser.parseMemberValue(AnnotationParser.java:280) ~[na:1.6.0_37]
    at sun.reflect.annotation.AnnotationParser.parseAnnotation(AnnotationParser.java:222) ~[na:1.6.0_37]
    at sun.reflect.annotation.AnnotationParser.parseAnnotations2(AnnotationParser.java:69) ~[na:1.6.0_37]
    at sun.reflect.annotation.AnnotationParser.parseAnnotations(AnnotationParser.java:52) ~[na:1.6.0_37]
    at java.lang.reflect.Field.declaredAnnotations(Field.java:1014) ~[na:1.6.0_37]
    at java.lang.reflect.Field.getDeclaredAnnotations(Field.java:1007) ~[na:1.6.0_37]

编辑:

有人提到我,在Win下使用NFS挂载时,JVM可能会决定卸载一个类,然后在需要时重新加载它。如果在此过程的中间,NFS 连接已断开,则文件句柄将无效,并且重新加载将失败,并显示类似的堆栈跟踪。在我们的例子中,我们使用的是Linux,所有涉及的文件都在同一个挂载中,这是一个本地硬盘。只是为了进行更多的测试,我已经CD进入Jetty临时目录,并手动删除了一个众所周知的特定服务类。如果 JVM 卸载它,然后尝试从类目录中重新装入它,它将失败。虽然这并不能解释最初的问题,但它可能会把更多的信息放在桌子上......


答案 1

这就是正在发生的事情:

  1. 当使用上面详述的 cmd 启动服务时,Jetty 会在“/tmp”下创建一个子目录,其中包含 JVM 加载的应用程序类和资源。
  2. 经过一段时间的不活动(在我们的特定方案中,在 13 到 20 天之间),该目录将消失。因此,JVM 无法加载该文件。我们仍然不知道JVM是否在此错误之前卸载了该类,或者为什么它试图重新读取*.class文件。查看源代码并了解这一点会很有趣,但这不在我们的短期待办事项列表中。
  3. 只需重新启动 Jetty 即可重新创建丢失的目录,并且服务再次启动。

我们得到的一个很好的提示是,有些人在Windows上通过NFS加载JAR中的资源时报告了类似的问题(如果网络连接在短时间内丢失,NFS句柄将失效,JVM将失败并出现类似的错误)。这不是我们的情况(/tmp是本地存储),但非常相似。

感谢大家的帮助。


答案 2

堆栈跟踪告诉我们,它是关于处理注释的,而不是与加载用于执行代码的类相关联的。注释处理器似乎尝试通过注释元素的值来解析注释成员的值ClassLoader

换句话说,您有一个值为类类型的批注,以及一个用这个构造批注的类或成员,但是在运行时无法通过带批注的元素访问该类。@Foo(xyz=ABC.class)ABCClassLoader

这与此类已通过另一个 类加载的事实并不冲突。ClassLoader


推荐