16

I want to cache large objects in JavaScript. These objects are retrieved by key, and it makes sense to cache them. But they won't fit in memory all at once, so I want them to be garbage collected if needed - the GC obviously knows better.

It is pretty trivial to make such a cache using WeakReference or WeakValueDictionary found in other languages, but in ES6 we have WeakMap instead, where keys are weak.

So, is it possible to make something like a WeakReference or make garbage-collected caches from WeakMap?

5
  • In JC it is assumed, that an object is immediately collected when it is no more needed. Hence, caching with WeakRefs does not make any sense. If you need to Cache large objects, use the Browser's Cache (if they come from a server) or sessionStorage. In a Node environment use Redis or Memcached. Commented Mar 31, 2018 at 10:40
  • @Tino: Your link contradicts your claim. Specifically: "As of 2012, all modern browsers ship a mark-and-sweep garbage-collector." and "As of 2019, it is not possible to explicitly or programmatically trigger garbage collection in JavaScript." Mark-and-sweep GCs do not reclaim memory immediately or even deterministically. Commented Jan 11, 2022 at 6:35
  • @j_random_hacker YMMV, but it would be a very dumb implementation of a MM to not immediately free a large object which is only held by a WeakRef (with refcount 0) and instead leave it all to Mark-and-Sweep. So the (my) assumption isn't entirely false. Today it is far more important to note, that WeakRefs are now supported by ES11 / ECMA-Script 2020 in all major browsers. But it still makes no sense to use WeakRef instead of some real caching implementation, so the accepted answer still holds. Commented Jan 12, 2022 at 20:46
  • @Tino: Thanks for noticing the arrival of WeakRefs, this is great news! I suggest writing an answer. However, everything else you wrote is either unclear or wrong. You claim "it still makes no sense to use WeakRef instead of some real caching implementation", but from the explainer doc linked to by your link: "A primary use for weak references is to implement caches or mappings holding large objects". What do you mean by "MM"? Memory manager? When M&S is being used, M&S is the memory manager, and doesn't do refcounting so it can't efficiently detect zero refcounts. Commented Jan 14, 2022 at 21:07
  • @Tino: WeakRefs were added to ES12/2021, according to para. 15 here. Commented Jan 15, 2022 at 2:45

4 Answers 4

6

It's now possible thanks to FinalizationRegistry and WeakRef

Example:

class WeakRefMap<K, V extends object> {
    private map = new Map<K, WeakRef<V>>();
    private finalizer = new FinalizationRegistry<K>((key) => this.map.delete(key));

    set(key: K, value: V): void {
        const oldRef = this.map.get(key);
        if (oldRef) {
            if (oldRef.deref() === value) return;
            this.finalizer.unregister(oldRef);
        }
        const newRef = new WeakRef(value);
        this.map.set(key, newRef);
        this.finalizer.register(value, key, newRef);
    }

    get(key: K): V | undefined {
        return this.map.get(key)?.deref();
    }
}

Note: If you are using an object type as key, avoid referencing the value inside the key which would cause memory leaks. Just to be safe, I would recommend only allowing string(s) as keys.

class WeakRefMap<K extends string, V extends object>
Sign up to request clarification or add additional context in comments.

4 Comments

An example somewhat similar to this exists on MDN. Grep for "Every time after a value is garbage collected"
Interesting. Unfortunately, every item I put into that map is immediately finalized by the GC as soon as I remove my last reference to it, which completely defeats the purpose of this map as a cache. It is almost always completely empty, even though there is more than enough memory available (tested with --max-old-space-size=4096). The same happened with every other weak-value-map implementation I could find out there. Is there a way to tell NodeJS GC to net be that eager when collecting orphaned objects?
@marc-guenther I think you can place a reference to the value inside a setTimeout, until setTimeout function is called and freed it will keep your value alive. Works on any runtime or browser. Don't know if there is NodeJS way to do it.
(i know it has been a long time but i have to take this out) As an addition to what i said earlier: That's the purpose of it anyway, You don't wanna create a new instance of something if there is already an existing one. That's the purpose of this cache and what the question is. For example you can cache a post, this way if the post id is the same you always get the same post object, or signal etc. So when you edit it you can see the change everywhere, and you dont have to deal with clearing the cache based on time or counters, you just leave it to the GC.
4

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).

5 Comments

I think this question is specifically referring to JavaScript, not Java or some other language which has WeakReference/WeakHashMap. The problem is that JavaScript only has something like a WeakHashMap, but not the equivalent of a WeakReference.
@Qantas94Heavy: Indeed so. Perhaps I should have indicated that JavaScript supports only the first, which is unfortunately not the one I suspect the OP really wants. Do you like the edit?
Your (2) doesn't require weak refs, and you omit a 3rd scenario, that being the exact one the OP describes (caching things). In this scenario, you need keys that are not themselves object references but which can be used to (re-)generate the cached thing (e.g., they could be URLs to redownload, RNG seeds to start a computation with, etc.).
Using a strong map in scenario #2 may result in a table that ends up holding useless references to many useless objects to which no other references exist, thus preventing reclamation of storage used by those objects. As for the caching scenario you describe, weak hash maps are identity-based. If one wishes to e.g. have a mapping from objects that contain [shapeId,size] value pairs to bitmaps, there would be no way for code given such an object and a weak map to determine whether the weak map contained any object holding those same values.
For scenario #2, using only WeakRefs (which apparently now do exist in JS, yay!) would mean that shared subtrees could get GCed out of existence at any time, so to prevent this you would need to keep at least one non-weak ref to each such subtree, and at that point it's simpler to just use non-weak refs for all of them. For the caching scenario, I think you're describing a limitation of the existing implementation of JS WeakMap, but not something that's intrinsically unachievable -- the concept of a value-based data structure that does this is sensible and useful.
3

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

AFAIK the answer is "no" to both questions.

Comments

0

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); 

1 Comment

Appreciate the effort, but get() pushes into a list, burning memory per call to get() until the referenced object is deleted. Even if you changed get() to trim off entries from the front that are now outside secondsToKeepACountFor from the current time, a tight loop calling get() could still blow up in memory. Also, you don't need a WeakMap for itemsToCounts -- just use a regular Map indexed by the same keys as you use for internalMap.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.