我们有一个自定义类加载器,称为here MainClassLoader
,它位于Java Web 应用程序之上(特别是在父类加载器为 的Tomcat 7 上WebAppClassLoader
)。这个自定义类加载器被设置为 Web 应用程序的 TCCL,其目的是将类路径资源(包括类和非类资源)的查找委托给一组其他自定义类加载器,每个类加载器代表一个可插入的模块到应用。(MainClassLoader
本身不加载任何内容。)
MainClassLoader.loadClass()
将进行 parent-first 委托,并在 aClassNotFoundException
上一个一个地通过可插入的子类加载器来查看它们中的哪一个将提供结果。如果它们都不能,则抛出ClassNotFoundException
.
然而,这里的逻辑有点复杂,再加上我们的最终用户可能最终插入了几个(10 年代)这些子模块,我们发现类加载器最终是其中之一考虑到当今 Java 对基于反射的命令模式实现的依赖程度,应用程序的 CPU 密集程度更高的部分。(我的意思是在运行时有很多Class.forName()
调用来加载和实例化类。)
我们首先在应用程序的定期线程转储中注意到这一点,以捕捉应用程序“正在运行”以查看它在做什么,并通过 JProfiler 分析某些已知比预期慢的用例。
我编写了一个非常简单的缓存方法,用于MainClassLoader
将loadClass()
(包括 a ClassNotFoundException
)调用的结果缓存在具有弱值(由 String className 键控)的并发映射中,并且此类的性能足够高以至于完全下降JProfiler 的热点列表。
但是,我担心我们是否真的可以安全地做到这一点。这样的缓存会妨碍预期的类加载器逻辑吗?这样做可能会遇到哪些陷阱?
我预期的一些明显的:
(1) 内存——显然这个缓存会消耗内存,如果没有限制,可能会消耗内存。我们可以使用有限的缓存大小来解决这个问题(我们使用谷歌的 Guava CacheBuilder 来处理这个缓存)。
(2) 动态类加载,尤其是在开发中 - 因此,如果在我们的缓存有一个陈旧的结果之后将新的或更新的类/资源添加到类路径中,这会使系统感到困惑,可能导致更频繁地在ClassNotFoundExceptions
类现在应该被抛出时可加载。缓存的“未找到”状态元素上的一个小 TTL 在这里可能会有所帮助,但我更关心的是,在开发过程中,当我们更新一个类并将它热交换到 JVM 时会发生什么。这个类很可能在一个类加载器中MainClassLoader
委托给,因此它的缓存可能有一个陈旧(旧)版本的类。但是,由于我使用的是弱值,这是否有助于缓解这种情况?我对弱引用的理解是,即使有资格收集它们也不会消失,直到 GC 运行通过并决定回收它们。
这是我对这种方法的两个已知问题/担忧,但让我害怕的是,当你在这里做非标准的事情时,类加载有点像黑魔法(如果不是黑暗科学),它充满了陷阱。
那么我不担心我应该担心的是什么?
更新/编辑
我们最终选择不按照我上面的原型进行本地缓存(它看起来很危险并且与 JVM 完成的缓存/优化相比是多余的),但在我们的 loadClass() 方法中做了一些优化。基本上我们在这个 loadClass() 方法中的逻辑(见下面的评论)没有遵循代码中的“最佳情况”路径,例如,当没有“定制”模块时,我们仍然表现得像尽管有,但让该类加载器抛出 ClassNotFoundException 并捕获它并进行下一次检查。这种模式意味着给定的类加载操作几乎总是要经过至少 3 个 try/catch 块,每个块中都会抛出 ClassNotFoundException。相当昂贵。
但是,我仍然想对我原来的问题发表评论,以帮助保持问题的存在并得到回答。
除了我已经列出的那些之外,在自定义类加载器中进行我们自己的缓存还有什么问题?