Garbage-collected cache via Javascript WeakMaps

折月煮酒 提交于 2019-12-04 01:46:01

is it possible to make WeakReference from WeakMap or make garbage-collected cache from WeakMap ?

AFAIK the answer is "no" to both questions.

There are two scenarios where it's useful for a hash map to be weak (yours seems to fit the second):

  1. One wishes to attach information to an object with a known identity; if the object ceases to exist, the attached information will become meaningless and should likewise cease to exist. JavaScript supports this scenario.

  2. One wishes to merge references to semantically-identical objects, for the purposes of reducing storage requirements and expediting comparisons. Replacing many references to identical large subtrees, for example, with references to the same subtree can allow order-of-magnitude reductions in memory usage and execution time. Unfortunately JavaScript doesn't support this scenario.

In both cases, references in the table will be kept alive as long as they are useful, and will "naturally" become eligible for collection when they become useless. Unfortunately, rather than implementing separate classes for the two usages defined above, the designers of WeakReference made it so it can kinda-sorta be usable for either, though not terribly well.

In cases where the keys define equality to mean reference identity, WeakHashMap will satisfy the first usage pattern, but the second would be meaningless (code which held a reference to an object that was semantically identical to a stored key would hold a reference to the stored key, and wouldn't need the WeakHashMap to give it one). In cases where keys define some other form of equality, it generally doesn't make sense for a table query to return anything other than a reference to the stored object, but the only way to avoid having the stored reference keep the key alive is to use a WeakHashMap<TKey,WeakReference<TKey>> and have the client retrieve the weak reference, retrieve the key reference stored therein, and check whether it's still valid (it could get collected between the time the WeakHashMap returns the WeakReference and the time the WeakReference itself gets examined).

As the other answers mentioned, unfortunately there's no such thing as a weak map, like there is in Java / C#.

As a work around, I created this CacheMap that keeps a maximum number of objects around, and tracks their usage over a set period of time so that you:

  1. Always remove the least accessed object, when necessary
  2. Don't create a memory leak.

Here's the code.

"use strict";

/**
 * This class keeps a maximum number of items, along with a count of items requested over the past X seconds.
 * 
 * Unfortunately, in JavaScript, there's no way to create a weak map like in Java/C#.  
 * See https://stackoverflow.com/questions/25567578/garbage-collected-cache-via-javascript-weakmaps
 */
module.exports = class CacheMap {
  constructor(maxItems, secondsToKeepACountFor) {
    if (maxItems < 1) {
      throw new Error("Max items must be a positive integer");
    }
    if (secondsToKeepACountFor < 1) {
      throw new Error("Seconds to keep a count for must be a positive integer");
    }

    this.itemsToCounts = new WeakMap();
    this.internalMap = new Map();
    this.maxItems = maxItems;
    this.secondsToKeepACountFor = secondsToKeepACountFor;
  }

  get(key) {
    const value = this.internalMap.get(key);
    if (value) {
      this.itemsToCounts.get(value).push(CacheMap.getCurrentTimeInSeconds());
    }
    return value;
  }

  has(key) {
    return this.internalMap.has(key);
  }

  static getCurrentTimeInSeconds() {
    return Math.floor(Date.now() / 1000);
  }

  set(key, value) {
    if (this.internalMap.has(key)) {
      this.internalMap.set(key, value);
    } else {
      if (this.internalMap.size === this.maxItems) {
        // Figure out who to kick out.
        let keys = this.internalMap.keys();
        let lowestKey;
        let lowestNum = null;
        let currentTime = CacheMap.getCurrentTimeInSeconds();
        for (let key of keys) {
          const value = this.internalMap.get(key);
          let totalCounts = this.itemsToCounts.get(value);
          let countsSince = totalCounts.filter(count => count > (currentTime - this.secondsToKeepACountFor));
          this.itemsToCounts.set(value, totalCounts);
          if (lowestNum === null || countsSince.length < lowestNum) {
            lowestNum = countsSince.length;
            lowestKey = key;
          }
        }

        this.internalMap.delete(lowestKey);
      }
      this.internalMap.set(key, value);
    }
    this.itemsToCounts.set(value, []);
  }

  size() {
    return this.internalMap.size;
  }
};

And you call it like so:

// Keeps at most 10 client databases in memory and keeps track of their usage over a 10 min period.
let dbCache = new CacheMap(10, 600); 
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!