当前位置: 首页 > 科技观察

如何追踪一个JS对象是否被GC_0

时间:2023-03-18 21:21:06 科技观察

在一个内置垃圾回收的语言中,开发者往往不需要过多关注内存管理。但这并不意味着我们可以完全忽略它。因为语言引擎的垃圾回收有一定的判断规则,如果我们的变量引用的内存不满足这个规则,那么引擎就不能自动回收这些内存。那么如何跟踪变量的内存是否被回收也很重要,尤其是在Node.js中。因为Node.js通常是长期作为服务端,一旦服务出现内存泄漏,就意味着我们的服务迟早会挂掉。虽然可以自动重启服务,但这并不能从根本上解决问题。那么如何检测内存泄漏就变得非常重要。我们通常使用V8自带的堆快照来判断一些变量的内存是否没有被正确回收。这是一个非常有效的手段,因为我们可以在堆快照中实时看到当前所有JS对象的存活情况。但是快照是一个非常繁重的操作,因为它不仅会阻塞线程的执行,还会导致内存暴涨。前者导致我们的服务暂时无法使用。具体时间取决于进程的堆大小,堆内存太大,收集堆快照引起的内存激增可能会导致进程直接挂掉。下面介绍一种轻量级的内存泄漏检测方法。虽然没有堆快照那么强大,但是在某些场景下还是很有用的。当我们想知道一个对象是否被回收时,有几种方法。第一种是通过引擎提供的快照能力直接检查对象的存活情况。二是注册对象为GC时的回调。下面是介绍的第二个能力。引擎并没有直接提供对象被GC时回调的能力,但是我们可以通过引擎提供的弱引用技术来实现这个功能(参考Node.js的源码)。const{createHook,AsyncResource}=require('async_hooks');constweakMap=newWeakMap();letgcCallbackContext={};lethooks;functiontrackGC(obj,gcCallback){if(!hooks){hooks=createHook({destroy(id){if(gcCallbackContext[id]){gcCallbackContext[id]();deletegcCallbackContext[id];}}}).enable();}constgcTracker=newAsyncResource('none');gcCallbackContext[gcTracker.asyncId()]=gcCallback;weakMap.set(obj,gcTracker);}然后分析代码的实现,主要是利用WeakMap和async_hooks来实现这个功能。当我们需要跟踪一个对象是否被GC时,只需要传入对象和一个回调,然后调用trackGC。trackGC会首先为被跟踪对象生成关联的AsyncResource对象。并记录AsyncResourceid和callback的对应关系,然后通过WeakMap将被跟踪对象与AsyncResource对象关联起来。那么当被跟踪对象失去所有引用时,其关联的AsyncResource对象就会被回收,async_hooks的destroy钩子会被回调。这时候会执行开发者注册的回调,通知开发者该对象已经被GC了。让我们看看如何使用它。const{trackGC}=require('../index');functionmemory(){return~~(process.memoryUsage().heapUsed/1024/1024);}console.log(`beforenewArray:${memory()}MB`);letkey={a:newArray(1024*1024*10)};letkey2={a:newArray(1024*1024*10)};console.log(`afternewArray:${memory()}MB`);trackGC(key,()=>{console.log("keygc");});trackGC(key2,()=>{console.log("key2gc");});global.gc();key=null;key2=null;global.gc();console.log(`aftergc:${memory()}MB`);上面的例子中,先打印初始化进程内存,然后分配一块大内存,注册对象的GC回调,将变量赋值为null,使其关联的对象失去唯一的强引用,从而被GC,最后执行一次明确的GC并在此时输出Memory。下面是我电脑上的输出。beforenewArray:3MBafternewArray:163MBaftergc:2MBkeygckey2gc可以看到注册的GC回调已经执行,内存确实已经回收了。最后分析一下这个实现。这主要是利用async_hooks模块的能力,因为WeakMap没有提供回调机制。下面看一下AsyncResource的实现,只列出核心代码。constructor(type,opts=kEmptyObject){constasyncId=newAsyncId();这个[async_id_symbol]=asyncId;这个[trigger_async_id_symbol]=triggerAsyncId;registerDestroyHook(this,asyncId,...);}在创建AsyncResource对象时,会调用registerDestroyHook。classDestroyParam{public:doubleasyncId;环境*环境;全局<对象>目标;GlobalpropBag;};staticvoidRegisterDestroyHook(constFunctionCallbackInfo&args){Isolate*isolate=args.GetIsolate();*p=newDestroyParam();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对象保存一些context,然后利用V8的弱导入对象注册一个回调机制来设置被跟踪对象的GC回调。然后当对象失去所有强引用并被GCed时,回调将被执行。voidAsyncWrap::WeakCallback(constWeakCallbackInfo&info){HandleScope范围(info.GetIsolate());std::unique_ptrp{info.GetParameter()};Localprop_bag=PersistentToLocal::Default(info.GetIsolate(),p->propBag);本地<值>val;p->env->RemoveCleanupHook(DestroyParamCleanupHook,p.get());如果(!prop_bag.IsEmpty()&&!prop_bag->Get(p->env->context(),p->env->destroyed_string()).ToLocal(&val)){返回;}if(val.IsEmpty()||val->IsFalse()){AsyncWrap::EmitDestroy(p->env,p->asyncId);}}最后EmitDestroy回调JS层执行destroyhook。这实现了跟踪一个JS对象是否被GC的能力。详情请参考https://github.com/theanarkh/gc-tracker。