如何追踪 JS 对象是否被 GC

更新日期: 2022-08-08阅读: 875标签: node

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

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

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

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

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 的 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<Object> target;
  Global<Object> propBag;};static void RegisterDestroyHook(const FunctionCallbackInfo<Value>& args) {


  Isolate* isolate = args.GetIsolate();
  DestroyParam* p = new DestroyParam();
  p->asyncId = args[1].As<Number>()->Value();
  p->env = Environment::GetCurrent(args);
  p->target.Reset(isolate, args[0].As<Object>());
  p->target.SetWeak(p, AsyncWrap::WeakCallback, WeakCallbackType::kParameter);
  p->env->AddCleanupHook(DestroyParamCleanupHook, p);
}

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

void AsyncWrap::WeakCallback(const WeakCallbackInfo<DestroyParam>& info) {
  HandleScope scope(info.GetIsolate());


  std::unique_ptr<DestroyParam> p{info.GetParameter()};
  Local<Object> prop_bag = PersistentToLocal::Default(info.GetIsolate(),
                                                      p->propBag);
  Local<Value> val;


  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。

来源: 编程杂技

链接: https://fly63.com/article/detial/11988

关于 Node.js 里 ES6 Modules 的一次更新说明

关于 Node.js 里 ES6 Modules 的一次更新说明,总结来说:CommonJS 与 ES6 Modules 之间的关键不同在于代码什么时候知道一个模块的结构和使用它。

用node.js开发一个可交互的命令行应用

在这个教程中,我们会开发一个命令行应用,它可以接收一个 CSV 格式的用户信息文件,教程的内容大纲:“Hello,World”,处理命令行参数,运行时的用户输入,异步网络会话,美化控制台的输出,封装成 shell 命令,JavaScript 之外

Node启动https服务器

首先你需要生成https证书,可以去付费的网站购买或者找一些免费的网站,可能会是key或者crt或者pem结尾的。不同格式之间可以通过OpenSSL转换

nodejs 异步转同步

nodej项目在微信环境开发,nodejs的异步特效,会导致请求没有完成就执行下面的代码,出现错误。经过多方查找,可以使用async模块来异步转同步,只有前一个function执行callback,下一个才会执行。

基于node服务器的大文件(G级)上传

3G的大文件分1500个2M二进度文件,通post方法发送给node服务,服务器全部接收到文件后,进组装生成你上文件。

为什么要把 JavaScript 放到服务器端上运行?

JavaScript比C的开发门槛要低,尽管服务器端JavaScript存在已经很多年了,但是后端部分一直没有市场,JavaScript在浏览器中有广泛的事件驱动方面的应用,考虑到高性能、符合事件驱动、没有历史包袱这3个主要原因,JavaScript成为了Node的实现语言。

了解node.js事件循环

node.js的第一个基本论点是I / O的性能消耗是很昂贵。因此,使用当前编程技术的最大浪费来自于等待I / O完成。有几种方法可以处理性能影响

Node.js 应用:Koa2 使用 JWT 进行鉴权

在前后端分离的开发中,通过 Restful API 进行数据交互时,如果没有对 API 进行保护,那么别人就可以很容易地获取并调用这些 API 进行操作。那么服务器端要如何进行鉴权呢?

Node.js 前端开发指南

我们经常跟Node.js打交道,即使你是一名前端开发人员 -- npm脚本,webpack配置,gulp任务,程序打包 或 运行测试等。即使你真的不需要深入理解这些任务,但有时候你会感到困惑,会因为缺少Node.js的一些核心概念而以非常奇怪的方式来编码。

happypack提升项目构建速度

运行在 Node.js 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。happypack把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!