【问题标题】:Node.js garbage collection and synchronized objectsNode.js 垃圾收集和同步对象
【发布时间】:2018-04-12 01:27:42
【问题描述】:

这有点难以解释,但我会试一试:

在 node.js 服务器应用程序中,我想处理一次可以在多个地方使用的数据对象。主要问题是,这些对象仅由对象 id 引用,并且是从数据库中加载的。

但是,一旦一个对象已经加载到一个作用域中,它就不应在请求时再次加载,而应返回相同的对象。

这让我想到了垃圾回收的问题:一旦某个对象在任何范围内不再需要,就应该完全释放它,以防止整个数据库一直在服务器的内存中。但问题就从这里开始:

我可以想到两种方法来创建这样的场景:要么使用全局对象引用(这会阻止任何对象被收集),要么真的复制这些对象,但以每次在其中的属性时同步它们的方式一个范围发生更改,通知其他实例有关该更改。

同样,因此每个实例都必须注册一个事件处理程序,该事件处理程序反过来又指向该实例,从而防止它再次被收集。

有没有人为我没有意识到的这种情况提出解决方案?还是我对垃圾收集器的理解有什么误解?

我要避免的是对内存中的每个对象进行手动引用计数。每次从任何集合中删除对象时,我都必须手动调整引用计数(js 中甚至没有析构函数或“引用减少”事件)

【问题讨论】:

  • 所以……你想要一个缓存。您可以限制缓存中的项目数量,丢弃最近最少使用的项目。如果您知道不再需要它们,也可以提前删除它们,但这不是可以自动完成的。
  • “缓存”并不是一个完全正确的概念,它应该是没有对象被加载两次,一旦它已经在内存中。在大小有限的高速缓存中,这是无法保证的。从缓存中删除的项目(但可能仍会在其他地方引用),加载两次。当然,我可以自己跟踪任何删除,但我相信一定有更优雅的方法,因为一旦你在某处删除对象时忘记减少引用计数器,你的内存泄漏就在那里。这正是 GC 应该避免的。
  • 是什么决定了你什么时候不再需要引用一个对象呢?当它不存在于任何“集合”中时?什么是集合?
  • 一旦一个对象从它所使用的任何作用域中释放出来,它就可以被收集(也包括数据)并在某个地方再次需要它时重新加载。问题是我想避免内存中同一对象的不同版本,因此只有一个实例。第二个优点是每个使用这个数据对象的对象都会知道最新的更新版本(因为它们都使用同一个对象而不是它自己的副本)。在 node.js 中没有真正的并行执行(或更好:调度)之后,竞争条件不再是问题
  • @Psi Redis 最常见的用例之一是作为应用程序和数据库之间的缓存。您将继续按照您目前的方式使用您的数据库。当您需要一个对象时,您可以从 Redis 中获取它。如果不存在,则将对象放入 Redis 中,然后使用它。不确定您要做什么,但 Redis 经常被用作在多个应用程序服务器之间共享状态的一种方式。您的用例听起来很相似。

标签: javascript node.js garbage-collection


【解决方案1】:

使用weak 模块,我实现了一个WeakMapObj,就像我们最初希望WeakMap 一样工作。它允许您对键使用原语,对数据使用对象,并且使用弱引用保留数据。并且,当它们的数据被 GCed 时,它会自动从地图中删除项目。结果证明是相当简单的。

const weak = require('weak');

class WeakMapObj {
    constructor(iterable) {
        this._map = new Map();
        if (iterable) {
            for (let array of iterable) {
                this.set(array[0], array[1]);
            }
        }
    }

    set(key, obj) {
        if (typeof obj === "object") {
            let ref = weak(obj, this.delete.bind(this, key));
            this._map.set(key, ref);
        } else {
            // not an object, can just use regular method
            this._map.set(key, obj);
        }
    }

    // get the actual object reference, not just the proxy
    get(key) {
        let obj = this._map.get(key);
        if (obj) {
            return weak.get(obj);
        } else {
            return obj;
        }
    }

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

    clear() {
        return this._map.clear();
    }

    delete(key) {
        return this._map.delete(key);
    }
}

我能够在测试应用程序中对其进行测试,并确认它在垃圾收集器运行时按预期工作。仅供参考,仅使一两个对象符合垃圾收集条件并不会导致垃圾收集器在我的测试应用程序中运行。我不得不强行调用垃圾收集器才能看到效果。我认为这在真正的应用程序中不会成为问题。 GC 将在需要时运行(它可能仅在有合理的工作量时运行)。


您可以使用这个更通用的实现作为对象缓存的核心,其中项目将保留在 WeakMapObj 中,直到它不再被其他地方引用。


这是一个使地图完全私有的实现,因此无法从WeakMapObj 方法之外访问它。

const weak = require('weak');

function WeakMapObj(iterable) {
    // private instance data
    const map = new Map();

    this.set = function(key, obj) {
        if (typeof obj === "object") {
            // replace obj with a weak reference
            obj = weak(obj, this.delete.bind(this, key));
        }
        map.set(key, obj);

    }

    // add methods that have access to "private" map
    this.get = function(key) {
        let obj = map.get(key);
        if (obj) {
            obj = weak.get(obj);
        }
        return obj;
    }

    this.has = function(key) {
        return map.has(key);
    }

    this.clear = function() {
        return map.clear();
    }

    this.delete = function(key) {
        return map.delete(key);
    }

    // constructor implementation    
    if (iterable) {
        for (let array of iterable) {
            this.set(array[0], array[1]);
        }
    }
}

【讨论】:

  • 非常好!比处理手动引用计数要优雅得多。
  • @Psi - 是的,weak 模块非常酷,而且它在 node.js 中似乎也很好用。有人想知道为什么语言本身没有这种类型的weakMap。我看到很多理由使用这样的东西。
  • 这就是我问这个问题的原因,我真的不敢相信没有内置功能可以做到这一点。对于 GC:它仅在满足某些条件(例如内存峰值、时间)时运行,因此您必须等待大约 10-20 分钟才能启动 GC。强制收集是一种更好的测试方式.但是,我不建议在生产代码中使用 GC,但出于测试目的,它非常有用
  • @Psi - 我对此实现进行了更新以覆盖.get(),以便它获取实际对象,而不仅仅是弱代理。我认为这是需要使用的,因为现在当有人调用 cache.get(id) 时,它会返回对实际对象的实时引用(这可以防止它在使用时被 GC)。如果没有这个覆盖,它只会返回弱代理,当您尝试使用它的代理时,它不会阻止原始代理被 GCed,这很容易导致问题。完整的实现还需要对.entries().forEach() 执行相同的覆盖。和.values()
  • 对,没想到但很明显
【解决方案2】:

听起来像是Map 对象的作业,该对象用作缓存,将对象存储为值(连同计数),并将 ID 作为键。当你想要一个对象时,你首先在Map 中查找它的 ID。如果在那里找到它,则使用返回的对象(将由所有人共享)。如果在那里找不到,则从数据库中获取它并将其插入Map(供其他人查找)。

然后,为了使Map 不会永远增长,从Map 获取某些内容的代码还需要从Map 释放一个对象。当 useCnt 在发布时变为零时,您将从 Map 中删除一个对象。

这可以通过创建某种包含Mapcache对象对调用者完全透明,并具有获取对象或释放对象的方法,它将完全负责维护refCntMap 中的每个对象上。

注意:您可能必须仔细编写从数据库中获取数据并将其插入Map 的代码,以免产生竞争条件,因为从数据库中获取数据可能是异步的,您可能会获得多个调用者都没有在Map 中找到它,并且都在从数据库中获取它的过程中。如何避免这种竞争条件取决于您拥有的确切数据库以及您如何使用它。一种可能性是第一个调用者在Map 中插入一个占位符,这样后续调用者就会知道在对象插入Map 并可供他们使用之前等待一些承诺解决。

以下是关于 ObjCache 如何工作的总体思路。当你想检索一个项目时,你调用cache.get(id)。这总是返回一个解析为对象的承诺(或者如果从数据库中获取它时出错,则拒绝)。如果对象已经在缓存中,它返回的 promise 将已经被解析。如果对象尚未在缓存中,则当从数据库中获取该对象时,promise 将解析。即使您的代码的多个部分请求一个“正在”从数据库中获取的对象,这也有效。当从数据库中检索到对象时,它们都会得到相同的承诺,即使用相同的对象解决。每次调用cache.get(id) 都会增加缓存中该对象的refCnt

然后,当给定的一段代码对对象完成时,您调用cache.release(id)。如果refCnt 达到零,这将减少内部refCnt 并从缓存中删除对象。

class ObjCache() {
    constructor() {
        this.cache = new Map();
    }
    get(id) {
        let cacheItem = this.cache.get(id);
        if (cacheItem) {
            ++cacheItem.refCnt;
            if (cacheItem.obj) {
                // already have the object
                return Promise.resolve(cacheItem.obj);
            }
            else {
                // object is pending, return the promise
                return cacheItem.promise;
            }
        } else {
            // not in the cache yet
            let cacheItem = {refCnt: 1, promise: null, obj: null};
            let p = myDB.get(id).then(function(obj) {
                // replace placeholder promise with actual object
                cacheItem.obj = obj;
                cacheItem.promise = null;
                return obj;
            });
            // set placeholder as promise for others to find
            cacheItem.promise = p;
            this.cache.set(id, cacheItem);
            return p;

        }
    }
    release(id) {
        let cacheItem = this.cache.get(id);
        if (cacheItem) {
            if (--cacheItem.refCnt === 0) {
                this.cache.delete(id);
            }
        }
    }
}

【讨论】:

  • 我刚读到WeakMaps,我相信,它们并不是我想要做的事情的完全解决方案。 WeakMap 存储一个对象(弱引用)作为键,并且能够在对象被收集时删除该值。但是,如果我只是将数据存储在对象本身中,这将是相同的。不幸的是,WeakMap 不能反过来工作。 Numbers 或 Strings 不允许用于 WeakMap 中的键
  • @Psi - 你是对的。 weakMap 不太适合您描述任务的方式。但是,你可以让一个常规的 Map 为它工作(我已经修改了我的答案),但是你必须在获取、使用和释放一个对象时进行手动 refCnt 。
  • 是的,但现在这正是我在编辑问题时所写的:我需要进行手动引用计数(即,每次从任何范围释放对象时,我都需要通知它发生这种情况)。我希望有另一种解决方法,因为这不是我使用 javascript 来再次处理手动引用计数的原因。
  • @Psi - 我不知道任何其他方式。我试图想办法让weakMap 为你工作,但我不知道怎么做。我在答案中添加了处理竞争条件的ObjCache() 的大纲。我假设你的数据库操作有一个占位符来检索不在缓存中的对象,并使用 Promise 来处理竞争条件。
  • 非常感谢,非常感谢您的努力。但是,要知道何时释放对象,我必须确保没有其他引用仍然存在。因此,每个引用该对象的对象都需要知道它何时被删除。
【解决方案3】:

好的,对于任何面临类似问题的人,我找到了解决方案。 jfriend00 通过提到 WeakMaps 将我推向了这个解决方案,这本身并不完全是解决方案,而是将我的注意力集中在弱引用上。

有一个简单称为weak 的 npm 模块可以解决问题。它持有一个对象的弱引用,并在对象被垃圾回收后安全地返回一个空对象(因此,有一种方法可以识别回收的对象)。

所以我使用DataObject 创建了一个名为WeakCache 的类:

class DataObject{

    constructor( objectID ){
        this.objectID = objectID;
        this.dataLoaded = new Promise(function(resolve, reject){
            loadTheDataFromTheDatabase(function(data, error){ // some pseudo db call
                if (error)
                {
                    reject(error);
                    return;
                }

                resolve(data);
            }); 
        });
    }

    loadData(){
        return this.dataLoaded;
    }   
}

class WeakCache{

    constructor(){
        this.cache = {};
    }

    getDataObjectAsync( objectID, onObjectReceived ){
        if (this.cache[objectID] === undefined || this.cache[objectID].loadData === undefined){ // object was not cached yet or dereferenced, recreate it

            this.cache[objectID] = weak(new DataObject( objectID )function(){
                // Remove the reference from the cache when it got collected anyway
                delete this.cache[this.objectID];
            }.bind({cache:this, objectID:objectID});
        }

        this.cache[objectID].loadData().then(onObjectReceived);
    }

}

这门课仍在进行中,但至少这是它可以工作的一种方式。唯一的缺点(但对于所有基于数据库的数据都是如此,双关语警告!因此没什么大不了的)是所有数据访问都必须是异步的。

这里会发生什么,缓存在某些时候可能对每个可能的对象 id 都持有一个空引用。

【讨论】:

  • 仅供参考,处理空引用的一种简单方法是使用 setInterval() 计时器,该计时器每隔一小时左右运行一次,并删除任何包含空引用的元素。只要删除它们的代码都是同步的,就不会导致任何可能的竞争条件。但是,也有可能使用弱回调来实时清理它。
  • 我以类似的方式思考,但在对象被释放时使用回调也有帮助。您可以将 objectID 绑定到回调函数,然后只需删除作为查找表中键的 objectID。
  • 我刚刚在weak 参考中读到,您必须非常小心回调,以免意外捕获回调范围内的对象本身,因为如果这样做,它永远不会是垃圾收集,因为它在技术上仍然可以通过实时代码“访问”。他们建议仅使用高级范围的回调函数。您提到在顶级回调中使用 .bind() 将是捕获 ID 的好方法,而不会在范围内捕获对象本身。
  • 没错。我正在使用您在此处看到的高级实现来运行我的测试,我目前可以验证的是它运行良好。幸运的是,使用 --expose_gc 可以强制 gc 进行清理,这是测试行为的好方法
  • 查看WeakMapObj 课程我提出了一个新的答案。我还必须使用--expose-gc 才能运行global.gc() 来测试它,它确实有效。很酷。
猜你喜欢
  • 1970-01-01
  • 2012-07-09
  • 2010-11-08
  • 1970-01-01
  • 2017-05-07
  • 2016-08-28
  • 1970-01-01
  • 1970-01-01
  • 2011-07-16
相关资源
最近更新 更多