Java 自定义委托类加载器缓存loadClass()结果是否安全?

Java 自定义委托类加载器缓存loadClass()结果是否安全?,java,caching,classloader,Java,Caching,Classloader,我们有一个自定义类加载器,在这里称为MainClassLoader,它位于Java web应用程序的顶部(特别是在Tomcat7上,父类加载器是WebAppClassLoader)。此自定义类加载器被设置为web应用程序的TCCL,其目的是将类路径资源(包括类和非类资源)的查找委托给一组其他自定义类加载器,每个自定义类加载器表示应用程序的可插入模块。(MainClassLoader本身不加载任何内容。) MainClassLoader.loadClass()将首先执行父类委托,在出现ClassN

我们有一个自定义类加载器,在这里称为
MainClassLoader
,它位于Java web应用程序的顶部(特别是在Tomcat7上,父类加载器是
WebAppClassLoader
)。此自定义类加载器被设置为web应用程序的TCCL,其目的是将类路径资源(包括类和非类资源)的查找委托给一组其他自定义类加载器,每个自定义类加载器表示应用程序的可插入模块。(
MainClassLoader
本身不加载任何内容。)

MainClassLoader.loadClass()
将首先执行父类委托,在出现
ClassNotFoundException
时,逐个检查可插入的子类加载器,以查看它们中的哪一个将提供结果。如果它们中没有一个可以,则抛出
ClassNotFoundException

然而,这里的逻辑要复杂一点,再加上我们的最终用户可能会插上几个(在10秒内)这些子模块,我们发现类加载器最终成为应用程序中CPU密集度更高的部分之一,考虑到Java现在对基于反射的命令模式实现的依赖程度。(我的意思是有很多
Class.forName()
调用在运行时加载和实例化类。)

我们首先在应用程序的周期性线程转储中注意到这一点,以捕获“运行中”的应用程序,查看它正在做什么,并通过JProfiler分析某些已知比预期慢的用例

我为
MainClassLoader
编写了一种非常简单的缓存方法,其中
loadClass()
(包括
ClassNotFoundException
)调用的结果缓存在具有弱值(由字符串className键入)的并发映射中,这门课的表现非常出色,完全脱离了JProfiler的热门话题列表

然而,我担心的是我们是否真的能安全地做到这一点。这样的缓存会妨碍预期的类加载器逻辑吗?这样做可能会有什么陷阱

我预计会有一些明显的问题:

(1) 内存-很明显,这个缓存会消耗内存,如果没有限制,可能会导致内存消耗。我们可以使用有限的缓存大小来解决这个问题(我们使用谷歌的Guava CacheBuilder来实现这个缓存)

(2) 动态类加载,特别是在开发中-因此,如果在缓存有过时的结果后将新的或更新的类/资源添加到类路径中,这将使系统混乱,可能导致在类现在应该可以加载时更频繁地抛出
ClassNotFoundExceptions
。在缓存的“未找到”状态元素上使用一个小TTL可能会有所帮助,但我更关心的是,在开发过程中,当我们更新一个类并将其热交换到JVM中时会发生什么。这个类很可能位于
MainClassLoader
委托给的类加载器中,因此它的缓存可能会有一个过时(旧)版本的类。然而,由于我使用的是弱值,这有助于缓解这种情况吗?我对弱引用的理解是,即使在符合收集条件的情况下,它们也不会消失,直到GC运行一个pass并决定回收它们

这是我对这种方法的两个已知问题/担忧,但让我害怕的是,类加载是一种黑色艺术(如果不是一门黑暗科学的话),当你在这里做非标准的事情时,它充满了陷阱

那么,我不担心的是什么呢

更新/编辑

我们最终选择了不进行我上面原型化的本地缓存(与JVM进行的缓存/优化相比,这似乎是危险和冗余的),但在loadClass()方法中进行了一些优化。基本上,我们在这个loadClass()方法中的逻辑(参见下面的注释)在代码中没有遵循“最佳情况”路径,例如,当没有“定制”模块时,我们的行为仍然像有,让该类加载器抛出ClassNotFoundException并捕获它并执行下一步检查。这个模式意味着一个给定的类加载操作几乎总是经过至少3个try/catch块,每个块中都会抛出一个ClassNotFoundException。相当贵。一些额外的代码用于确定是否存在与被委托的类加载器相关联的URL,允许我们绕过这些检查(以及由此产生的异常抛出/捕获),从而使此类的性能提高了近25000%

然而,我仍然想对我最初的问题发表评论,以帮助这个问题继续得到回答


在自定义类加载器中进行我们自己的缓存时,除了我已经列出的以外,还有什么需要考虑的呢?

我对您的实现描述有点困惑。首先,您通常不会重写
loadClass()
,您通常会重写
findClass()
(假设您维护的是父级第一次委托顺序)。其次,classloader impl应该为您缓存所有这些查找。第三,为什么您不断地对相同的类名调用
loadClass()
?很抱歉,没有完成-为了简洁起见,我试图进行总结。这里我们有一个条件委托模型,如果存在某些类型的插件模块,我们将在执行父委托之前先委托给这些模块。loadClass()首先调用findLoadedClass(),然后如果存在“定制”模块,则检查它们,然后执行父.loadClass(),然后执行任何其他插件模块。为了回答您的第三个问题,我们不是不断调用loadClass(),实际上是第三方库(BIRT)在执行此操作。我们的微基准显示,如果他们调用Class.forName(),这将很接近