当前位置:首页 > IT科技类资讯

如何追踪 JS 对象是否被 GC

在自带垃圾回收的何追语言中,开发者往往不需要过多地关注内存管理。象否但是何追不代表我们可以完全忽略它。因为语言引擎的象否垃圾回收是有一定的判断规则的,如果我们的何追变量所引用的内存没有符合这个规则,那么引擎无无法对这些内存进行自动回收。象否所以如何追踪变量的何追内存是否被回收也变得非常重要,尤其在 Node.js 中。象否

因为 Node.js 通常以服务器的何追角色长期提供服务,一旦服务发生内存泄露,象否就意味着我们的何追服务迟早会挂掉,尽管服务可以被自动重启,象否但是何追这并不能从根本上解决问题。所以如何检测内存泄露,象否就变得非常重要。何追

如何追踪 JS 对象是否被 GC

我们通常会使用 V8 自带的堆快照来判断某些变量的内存是否没有得到正确的回收,这是香港云服务器一种非常有效的手段,因为我们在堆快照中可以实时看到当前所有 JS 对象的存活情况。但是快照是一种非常重的操作,因为它不仅会阻塞线程的执行,而且会导致内存的暴涨,前者导致我们的服务出现短暂的不可用,具体时间取决于进程的堆大小,堆内存过大时,采集堆快照所引起的内存暴涨可能会导致进程直接挂掉。下面介绍一种轻量级的内存泄露检测方式,虽然它不像堆快照那么强大,但是在某些场景下是有用的。

如何追踪 JS 对象是否被 GC

当我们想知道一个对象有没有被回收时,有几种方式,第一种就是通过引擎提供的快照能力,直接查看对象的服务器托管存活情况,第二种则是注册对象被 GC 时的回调,下面是介绍的第二种能力。引擎没有直接提供当对象被 GC 时回调的能力,但是我们可以通过引擎提供的弱引用技术来实现这个功能(可参考 Node.js 的源码)。

如何追踪 JS 对象是否被 GC

const { createHook, AsyncResource } = require(async_hooks);

const weakMap = new WeakMap();

let gcCallbackContext = { };

let hooks;

function trackGC(obj, gcCallback) {

if (!hooks) {

hooks = createHook({

destroy(id) {

if (gcCallbackContext[id]) {

gcCallbackContext[id]();

delete gcCallbackContext[id];

}

}

}).enable();

}

const gcTracker = new AsyncResource(none);

gcCallbackContext[gcTracker.asyncId()] = gcCallback;

weakMap.set(obj, gcTracker);

}

接着分析下代码的实现,主要是利用了 WeakMap 和 async_hooks 实现了这个功能。当我们需要追踪一个对象是否被 GC 时,我们只需要传入这个对象和一个回调,然后调用 trackGC。trackGC 首先会针对一个被追踪的对象生成一个关联的 AsyncResource 对象。并且记录 AsyncResource id 和 回调的对应关系,然后把再通过 WeakMap 把被追踪的对象和 AsyncResource 对象关联起来。那么当被追踪的对象失去所有引用时,它关联的 AsyncResource 对象就会被回收,从而 async_hooks 的云南idc服务商 destroy 钩子被回调,这时候执行开发者注册的回调通知开发者该对象已经被 GC。接下来看看 如何使用。

const { trackGC } = require(../index);

function memory() {

return ~~(process.memoryUsage().heapUsed / 1024 / 1024);

}

console.log(`before new Array: ${ memory()} MB`);

let key = {

a: new Array(1024 * 1024 * 10)

};

let key2 = {

a: new Array(1024 * 1024 * 10)

};

console.log(`after new Array: ${ memory()} MB`);

trackGC(key, () => {

console.log("key gc");

});

trackGC(key2, () => {

console.log("key2 gc");

});

global.gc();

key = null;

key2 = null;

global.gc();

console.log(`after gc: ${ memory()} MB`);

在上面的例子中,首先打印出初始化的进程内存,接着分配一块大的内存,注册对象的 GC 回调,把变量赋值为 null 使得它的关联的对象失去唯一的强引用,从而被 GC,最后进行显式 GC 并输出这时候的内存。下面是我电脑上的输出。

before new Array: 3 MB

after new Array: 163 MB

after gc: 2 MB

key gc

key2 gc

可以看到注册的 GC 回调被执行了,并且内存的确被回收了。

最后分析一下这个实现。这里主要是利用了 async_hooks 模块的能力,因为 WeakMap 是没有提供回调机制的。来看一下 AsyncResource 的实现,只列出核心代码。

constructor(type, opts = kEmptyObject) {

const asyncId = newAsyncId();

this[async_id_symbol] = asyncId;

this[trigger_async_id_symbol] = triggerAsyncId;

registerDestroyHook(this, asyncId, ...);

}

当创建一个 AsyncResource 对象时,会调用 registerDestroyHook。

class DestroyParam {

public:

double asyncId;

Environment* env;

Global

Global

Isolate* isolate = args.GetIsolate();

DestroyParam* p = new DestroyParam();

p->asyncId = args[1].As()->Value();

p->env = Environment::GetCurrent(args);

p->target.Reset(isolate, args[0].As

p->target.SetWeak(p, AsyncWrap::WeakCallback, WeakCallbackType::kParameter);

p->env->AddCleanupHook(DestroyParamCleanupHook, p);

}

RegisterDestroyHook 首先创建了一个 DestroyParam 对象保存一些上下文,然后利用 V8 的弱引入对象可以注册回调的机制设置需要追踪的对象的 GC 回调。那么当对象失去所有强引用被 GC 时,回调就会被执行。

void AsyncWrap::WeakCallback(const WeakCallbackInfo& info) {

HandleScope scope(info.GetIsolate());

std::unique_ptrp{ info.GetParameter()};

Local

p->propBag);

Localval;

p->env->RemoveCleanupHook(DestroyParamCleanupHook, p.get());

if (!prop_bag.IsEmpty() &&

!prop_bag->Get(p->env->context(), p->env->destroyed_string())

.ToLocal(&val)) {

return;

}

if (val.IsEmpty() || val->IsFalse()) {

AsyncWrap::EmitDestroy(p->env, p->asyncId);

}

}

最终通过 EmitDestroy 回调 JS 层执行 destroy 钩子。这样就实现了追踪 JS 对象是否被 GC 的能力。具体可以参考 https://github.com/theanarkh/gc-tracker。

分享到:

滇ICP备2023006006号-16