我们有一个名为MainClassLoader
的自定义类加载器,位于Java Web应用程序之上(特别是在父类加载器为WebAppClassLoader
的Tomcat 7上)。此自定义类加载器设置为Web应用程序的TCCL,其目的是将类路径资源(包括类和非类资源)的查找委派给一组其他自定义类加载器,每个自定义类加载器代表一个可插入模块。应用。 (MainClassLoader
本身不加载任何内容。)
MainClassLoader.loadClass()
将执行父级优先委派,并在ClassNotFoundException
上逐个通过可插入的子类装入器查看哪些将提供结果。如果它们都不能,则会抛出ClassNotFoundException
。
然而,这里的逻辑有点复杂,并且结合我们的最终用户可能最终插入这些子模块的几个(在10个中)这一事实,我们发现类加载器最终鉴于Java如今依赖于基于反射的命令模式实现,它是应用程序中CPU密集型部分之一。 (我的意思是,有很多Class.forName()
调用在运行时加载和实例化类。)
我们首先在应用程序的定期线程转储中注意到这一点,以便抓住应用程序“正在运行”以查看它在做什么,并通过JProfiler分析已知比所需速度慢的某些用例。
我为MainClassLoader
编写了一个非常简单的缓存方法,其中loadClass()
(包括ClassNotFoundException
)调用的结果缓存在具有弱值的并发映射中(键入为String className),这个类的性能足够高,完全脱离了JProfiler的热点列表。
然而,我担心我们能否真的安全地做到这一点。这样的缓存是否会妨碍预期的类加载器逻辑?这样做会有什么陷阱?
我预计会有一些显而易见的:
(1)内存 - 显然这个缓存消耗内存,如果不受限制则可能是内存消耗。我们可以使用有限的缓存大小来解决这个问题(我们正在使用Google的Guava CacheBuilder进行此缓存)。
(2)动态类加载,特别是在开发中 - 所以如果在我们的缓存有陈旧结果后将新的或更新的类/资源添加到类路径中,这会使系统混淆,可能更常见于{{1}当类现在应该是可加载的时被抛出。缓存的“未找到”状态元素上的小TTL可能对此有所帮助,但我更关心的是,在开发期间,当我们更新类并将其热交换到JVM时会发生什么。这个类很可能是ClassNotFoundExceptions
委托给它的一个类加载器,因此它的缓存可能会有一个过时(较旧)的类版本。但是,由于我使用弱值,这有助于缓解这种情况吗?我对弱引用的理解是,即使有资格收集,它们也不会消失,直到GC运行一个决定收回它们的通行证。
这是我对这种方法的两个已知问题/担忧,但让我感到害怕的是,当你在这里做非标准的事情时,类加载是一种黑色艺术(如果不是黑暗的科学),它充满了陷阱。
那么我不担心我应该担心什么?
UPDATE /修改
我们最终选择不进行本地缓存,因为我在上面进行了原型设计(它看起来很危险,并且在JVM完成的缓存/优化方面是多余的),但在我们的loadClass()方法中进行了一些优化。基本上我们在这个loadClass()方法中的逻辑(参见下面的注释)并没有遵循代码中的“最佳情况”路径,例如它可能具有,例如当没有“自定义”模块到位时,我们仍然表现得像那样,让该类加载器抛出一个ClassNotFoundException并捕获它并进行下一次检查。这种模式意味着给定的类加载操作几乎总是经历至少3个try / catch块,每个块都抛出ClassNotFoundException。相当昂贵。一些额外的代码用于确定是否有任何与委托的类加载器相关联的URL允许我们绕过这些检查(以及由此产生的异常抛出/捕获),这使我们的性能提升了近25000%。
但是,我仍然希望对我原来的问题发表评论,以帮助解决这个问题。
在自定义类加载器中进行自己的缓存有什么顾虑,除了我已经列出的那些?