有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java如果缓存不能保留引用,如何缓存与类相关的方法对象

在这种情况下,我需要一个相对昂贵的操作来确定java类中方法的某个子集。为了优化,我想保留一个缓存,有点像这样:

private final static HashMap<Class<?>, Set<Method>> cache = new HashMap<>();

然而,我也在长时间运行的服务器环境中,我们希望类加载器来来去去去。上面的缓存不好,因为它会保留类,防止类加载器被垃圾收集

我第一次尝试修复是:

private final static WeakHashMap<Class<?>, Set<Method>> cache = new HashMap<>();

不幸的是,这也不起作用,因为集合中的方法对象保留了对类的硬引用,这意味着WeakHashMap的点丢失了

我试过其他几种方法。例如,我定义了一个数据结构,其中hold Method对象是WeakReference。我也不喜欢它,因为虽然该方法保留对该类的硬引用,但该类实际上并不保留对该方法的引用,这意味着我对该方法的WeakReference通常会从get()方法返回null(如果没有其他人最终保留集合中的一个方法)

我的具体问题是,在不保留对类的任何硬引用的情况下,实现从一个类到另一个方法集的缓存的好方法是什么


共 (2) 个答案

  1. # 1 楼答案

    Edit:我现在实现了下面我自己最奇特的建议——使用WeakReference并将带有强引用的类注入拥有缓存结果的类加载器:https://github.com/Legioth/reflectioncache


    我一直在研究同一类问题,我发现了几种方法,每种方法都有各自的优缺点。我将从需要缓存其结果的库的角度来描述这些

    有力的证明

    即使您保持对方法实例的强引用,也有一些方法可以处理类加载器泄漏问题

    选择性缓存

    仅缓存来自加载包含缓存的类的同一类加载器的值。在大多数服务器环境中,这意味着进行缓存的库应该与使用库的代码位于同一个.war中,除非还涉及OSGi。对于无法缓存的类,可以每次计算该值或引发异常

    在缓存命中后验证类加载器

    在某些特定情况下可能有效的脆弱方法是使用类。getName()字符串作为缓存键,并在使用旧值之前验证结果是否来自正确的类加载器。这里的诀窍是,如果类加载器发生了更改,用新的缓存项替换旧的缓存项。最终,所有旧条目都将以这种方式刷新,假设在重新部署后仍将使用相同的密钥,并且假设始终存在重新部署,而不只是停止使用缓存

    明确驱逐

    要求库的用户跟踪他们的类加载器何时将被丢弃,并通知库清除为该类加载器缓存的所有内容。这种模式的一个例子可以在java.util.ResourceBundle.clearCache(ClassLoader)中看到,尽管该类还提供了其他缓存管理机制

    定时驱逐

    我发现的涉及强引用的最后一种方法是删除已经有一段时间没有使用的缓存项。根据缓存数据的性质,您可以清除单个过期的entires,或者为每个遇到的类加载器保留一个时间戳,并逐出与该类加载器关联的所有条目。每次使用缓存时都可以执行简单的逐出操作,但是这种方法总是会泄漏最后一个用户的类加载器,因为在那之后就不会有任何人使用缓存了。解决这个问题需要一个计时器线程,除非仔细管理,否则它本身也可能是内存泄漏的来源

    软参考

    JVM(至少是Oracle/OpenJDK版本)已经提供了类似于在一段时间没有使用条目时逐出的方法:SoftReference。关于SoftRefLRUPolicyMSPerMB的文档表明,软引用是根据自上次访问以来的时间进行回收的,并根据可用内存量进行调整。这种方法的主要缺点是,如果系统运行接近其内存限制,将有许多缓存未命中

    软引用

    正如您已经发现的那样,直接使用WeakReference对方法不起作用,因为这样就没有任何东西可以阻止方法实例被收集。为了防止方法实例被收集,您需要确保至少有一个对它的强引用。为了防止强引用导致类加载器泄漏,它必须来自定义该方法的类的类加载器。此类引用最直接的来源是通过该类加载器加载的类中的静态字段。一旦有了这个字段,就可以将该类加载器中的值的实际缓存映射存储在其中。然后,管理缓存的类可以使用对实际地图的WeakReference

    用户提供的参考持有人

    根据图书馆的性质,要求他要求库用户为类提供这样一个静态字段,供库(ab)使用

    生成的参考持有人

    创建此引用的一种可能的方法是,使用反射为一个只包含一个静态字段的简单类使用字节码运行ClassLoader.defineClass,然后使用更多反射来更新该字段的值,如上所述。我还没有在实践中尝试过这一点,但如果真的有效,它似乎将是classloader缓存的圣杯

  2. # 2 楼答案

    嗯,我有很多想法:

    首先: 关于键,可以使用类名而不是类对象。然后验证生成的方法是否属于正确的类/类装入器

    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缓存

    第三:如果您的缓存是由应用程序类加载器加载的,那么应该没有任何问题

    第四:如果可以确定使用服务的服务器中类加载器(=应用程序)的数量以及要缓存的类的数量,那么使用标准缓存,只需调整它所包含元素的大小即可。缓存将根据逐出策略删除未使用的条目