我处于这样一种情况:我有一个相对昂贵的操作来确定java类的某个方法子集。为了优化,我想保留一个缓存,有点像这样:
private final static HashMap<Class<?>, Set<Method>> cache = new HashMap<>();
但是,我也在长期运行的服务器环境中,我们希望类加载器来来去去。上面的缓存并不好,因为它会保留类,防止类加载器被垃圾收集。
我第一次尝试修复是:
private final static WeakHashMap<Class<?>, Set<Method>> cache = new HashMap<>();
不幸的是,这也不起作用,因为集合中的Method对象将硬引用保留回Class,这意味着WeakHashMap的点丢失了。
我尝试了其他几件事。例如,我已经定义了一个数据结构,其中保存的Method对象是WeakReference。我也没有爱,因为虽然Method拥有一个返回Class的硬引用,但Class实际上并不包含对Method的引用,这意味着我对方法的WeakReference经常从get()方法返回null (如果没有其他人最终持有该集合中的一个方法)。
我的具体问题是什么是从类到方法集完成此缓存的好方法,而不保留对类的任何硬引用?
答案 0 :(得分:2)
编辑:我现在已经实现了下面我最自己的建议 - 使用WeakReference并将具有强引用的类注入拥有缓存结果的ClassLoader中:https://github.com/Legioth/reflectioncache < / p>
我一直在研究同样的问题,我发现了几种方法,每种方法各有利弊。我将从需要缓存其结果的库的角度来描述这些。
即使您保留对Method实例的强引用,也有一些方法可以处理类加载器泄漏问题。
仅缓存来自加载包含缓存的类的同一个类加载器的值。在大多数服务器环境中,这意味着执行缓存的库应与使用库的代码位于同一.war
,除非还涉及OSGi。对于无法缓存的类,您可以每次计算值或抛出异常。
在某些特定情况下可能起作用的脆弱方法是使用例如Class.getName()字符串作为缓存键,并在使用旧值之前验证结果是否来自右侧类加载器。这里的技巧是如果类加载器已更改,则用新的缓存条目替换旧的缓存条目。最终所有旧条目都将以这种方式刷新,假设重新部署后仍将使用相同的密钥,并假设总是重新部署,而不是仅停止使用缓存。
要求库的用户跟踪他们的类加载器何时被丢弃,并通知库清除为该类加载器缓存的所有内容。可以在java.util.ResourceBundle.clearCache(ClassLoader)
中看到此模式的示例,尽管该类还提供了其他缓存管理机制。
我发现涉及强引用的最后一种方法是删除一段时间未使用的缓存条目。根据缓存数据的性质,您可以清除单个过期的entires,或者只为每个遇到的类加载器保留一个时间戳,并逐出与该类加载器关联的所有条目。每次使用缓存时都可以进行简单的驱逐,但是这种方法总是会泄漏最后一个用户的类加载器,因为之后不会有人使用缓存。解决该问题需要一个计时器线程,除非仔细管理,否则它本身也可能是内存泄漏的来源。
JVM(至少是Oracle / OpenJDK版本)已经提供了类似于在一段时间内没有使用条目时驱逐的方法:SoftReference
。有关SoftRefLRUPolicyMSPerMB
的文档意味着SoftReference的回收基于自上次访问以来的时间长度,并根据可用内存量进行调整。这种方法的主要缺点是,如果系统运行接近其内存限制,将会有许多缓存未命中。
正如您已经发现的那样,直接使用方法的WeakReference不起作用,因为没有任何东西可以阻止收集Method实例。要防止收集Method实例,您需要确保至少有一个强引用。为了防止强引用导致类加载器泄漏,它必须来自定义Method的类的类加载器。这种引用最直接的来源是通过该类加载器加载的类中的静态字段。拥有该字段后,可以存储该类加载器中值的实际缓存映射。然后,管理缓存的类可以使用WeakReference来实际映射。
根据库的性质,可能需要库用户为库提供这样的静态字段以供(ab)使用。
创建此引用的一种可能方法是使用反射来运行带有字节码的ClassLoader.defineClass
,用于仅包含一个静态字段的简单类,然后使用更多反射来更新该字段的值,如上所述。我没有在实践中尝试过,但如果确实有效,似乎它将成为类加载器缓存的圣杯。
答案 1 :(得分:0)
第一: 关于键,您可以使用类名而不是类对象。然后验证生成的方法是否属于正确的类/类加载器。
class CacheEntry { Set<Method> methods; Class<?> klass; }
Cache<String, CacheEntry> cache = ...
Set<Method> getCachedMethods(Class<?> c) {
CacheEntry e = cache.get(klass.getName());
if ((e != null && e.klass != c) ||
e == null) {
e = recomputeEntry(c);
cache.put(c, e);
}
return e.methods;
}
好的,如果您经常有更多同名的课程,这将无效。
第二:使用带有弱键和值的google guava缓存。
第三:如果您的缓存由应用程序类加载器加载,那么应该没有问题....
第四:如果您可以使用您的服务确定服务器中的类加载器(=应用程序)的数量以及要缓存的类的数量,请使用标准缓存并调整它所包含的元素的大小。缓存将通过驱逐策略删除未使用的条目。