内容简介:memwatch是一个c++扩展,主要用来观察nodejs内存泄露问题,基本用法如下:分析的版本为这份配置表示其生成的目标是
memwatch是一个c++扩展,主要用来观察nodejs内存泄露问题,基本用法如下:
const memwatch = require('@airbnb/memwatch'); function LeakingClass() { } memwatch.gc(); var arr = []; var hd = new memwatch.HeapDiff(); for (var i = 0; i < 10000; i++) arr.push(new LeakingClass); var hde = hd.end(); console.log(JSON.stringify(hde, null, 2)); 复制代码
实现分析
分析的版本为 @airbnb/memwatch 。首先从binding.gyp开始入手:
{ 'targets': [ { 'target_name': 'memwatch', 'include_dirs': [ "<!(node -e \"require('nan')\")" ], 'sources': [ 'src/heapdiff.cc', 'src/init.cc', 'src/memwatch.cc', 'src/util.cc' ] } ] } 复制代码
这份配置表示其生成的目标是 memwatch.node
,源码是src目录下的 heapdiff.cc
、 init.cc
、 memwatch.cc
、 util.cc
,在项目编译的过程中还需要include额外的nan目录,nan目录通过执行 node -e "require('nan')
按照node模块系统寻找 nan
依赖, <!
表示后面是一条指令。
memwatch的入口函数在 init.cc
文件中,通过 NODE_MODULE(memwatch, init);
进行声明。当执行 require('@airbnb/memwatch')
的时候会首先调用 init
函数:
void init (v8::Handle<v8::Object> target) { Nan::HandleScope scope; heapdiff::HeapDiff::Initialize(target); Nan::SetMethod(target, "upon_gc", memwatch::upon_gc); Nan::SetMethod(target, "gc", memwatch::trigger_gc); Nan::AddGCPrologueCallback(memwatch::before_gc); Nan::AddGCEpilogueCallback(memwatch::after_gc); } 复制代码
init函数的入口参数 v8:Handle<v8:Object> target
可以类比nodejs中的 module.exports
的 exports
对象。函数内部做的实现可以分为三块,初始化target、给target绑定 upon_gc
和 gc
两个函数、在nodejs的gc前后分别挂上对应的钩子函数。
Initialize实现
到 heapdiff.cc
文件中来看 heapdiff::HeapDiff::Initialize(target);
的实现。
void heapdiff::HeapDiff::Initialize ( v8::Handle<v8::Object> target ) { Nan::HandleScope scope; v8::Local<v8::FunctionTemplate> t = Nan::New<v8::FunctionTemplate>(New); t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(Nan::New<v8::String>("HeapDiff").ToLocalChecked()); Nan::SetPrototypeMethod(t, "end", End); target->Set(Nan::New<v8::String>("HeapDiff").ToLocalChecked(), t->GetFunction()); } 复制代码
Initialize
函数中创建一个叫做 HeapDiff
的函数 t
,同时在 t
的原型链上绑了 end
方法,使得js层面可以执行 vat hp = new memwatch.HeapDiff();hp.end()
。
new memwatch.HeapDiff
实现
当js执行 new memwatch.HeapDiff();
的时候,c++层面会执行 heapdiff::HeapDiff::New
函数,去掉注释和不必要的宏,New函数精简如下:
NAN_METHOD(heapdiff::HeapDiff::New) { if (!info.IsConstructCall()) { return Nan::ThrowTypeError("Use the new operator to create instances of this object."); } Nan::HandleScope scope; HeapDiff * self = new HeapDiff(); self->Wrap(info.This()); s_inProgress = true; s_startTime = time(NULL); self->before = v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(NULL); s_inProgress = false; info.GetReturnValue().Set(info.This()); } 复制代码
可以看到用户在js层面执行 var hp = new memwatch.HeapDiff();
的时候,c++层面会调用nodejs中的v8的api对对堆上内存打一个snapshot保存到self->before中,并将当前对象返回出去。
memwatch.HeapDiff.End
实现
当用户执行 hp.end()
的时候,会执行原型链上的end方法,也就是c++的 heapdiff::HeapDiff::End
方法。同样去掉冗余的注释以及宏,End方法可以精简如下:
NAN_METHOD(heapdiff::HeapDiff::End) { Nan::HandleScope scope; HeapDiff *t = Unwrap<HeapDiff>( info.This() ); if (t->ended) { return Nan::ThrowError("attempt to end() a HeapDiff that was already ended"); } t->ended = true; s_inProgress = true; t->after = v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(NULL); s_inProgress = false; v8::Local<Value> comparison = compare(t->before, t->after); ((HeapSnapshot *) t->before)->Delete(); t->before = NULL; ((HeapSnapshot *) t->after)->Delete(); t->after = NULL; info.GetReturnValue().Set(comparison); } 复制代码
在End函数中,拿到当前的HeapDiff对象之后,再对当前的堆上内存再打一个snapshot,调用compare函数对前后两个snapshot对比后得到comparison后,将前后两次snapshot对象释放掉,并将结果通知给js。
下面分析下compare函数的具体实现: compare函数内部会递归调用buildIDSet函数得到最终堆快照的diff结果。
static v8::Local<Value> compare(const v8::HeapSnapshot * before, const v8::HeapSnapshot * after) { Nan::EscapableHandleScope scope; int s, diffBytes; Local<Object> o = Nan::New<v8::Object>(); // first let's append summary information Local<Object> b = Nan::New<v8::Object>(); b->Set(Nan::New("nodes").ToLocalChecked(), Nan::New(before->GetNodesCount())); //b->Set(Nan::New("time"), s_startTime); o->Set(Nan::New("before").ToLocalChecked(), b); Local<Object> a = Nan::New<v8::Object>(); a->Set(Nan::New("nodes").ToLocalChecked(), Nan::New(after->GetNodesCount())); //a->Set(Nan::New("time"), time(NULL)); o->Set(Nan::New("after").ToLocalChecked(), a); // now let's get allocations by name set<uint64_t> beforeIDs, afterIDs; s = 0; buildIDSet(&beforeIDs, before->GetRoot(), s); b->Set(Nan::New("size_bytes").ToLocalChecked(), Nan::New(s)); b->Set(Nan::New("size").ToLocalChecked(), Nan::New(mw_util::niceSize(s).c_str()).ToLocalChecked()); diffBytes = s; s = 0; buildIDSet(&afterIDs, after->GetRoot(), s); a->Set(Nan::New("size_bytes").ToLocalChecked(), Nan::New(s)); a->Set(Nan::New("size").ToLocalChecked(), Nan::New(mw_util::niceSize(s).c_str()).ToLocalChecked()); diffBytes = s - diffBytes; Local<Object> c = Nan::New<v8::Object>(); c->Set(Nan::New("size_bytes").ToLocalChecked(), Nan::New(diffBytes)); c->Set(Nan::New("size").ToLocalChecked(), Nan::New(mw_util::niceSize(diffBytes).c_str()).ToLocalChecked()); o->Set(Nan::New("change").ToLocalChecked(), c); // before - after will reveal nodes released (memory freed) vector<uint64_t> changedIDs; setDiff(beforeIDs, afterIDs, changedIDs); c->Set(Nan::New("freed_nodes").ToLocalChecked(), Nan::New<v8::Number>(changedIDs.size())); // here's where we'll collect all the summary information changeset changes; // for each of these nodes, let's aggregate the change information for (unsigned long i = 0; i < changedIDs.size(); i++) { const HeapGraphNode * n = before->GetNodeById(changedIDs[i]); manageChange(changes, n, false); } changedIDs.clear(); // after - before will reveal nodes added (memory allocated) setDiff(afterIDs, beforeIDs, changedIDs); c->Set(Nan::New("allocated_nodes").ToLocalChecked(), Nan::New<v8::Number>(changedIDs.size())); for (unsigned long i = 0; i < changedIDs.size(); i++) { const HeapGraphNode * n = after->GetNodeById(changedIDs[i]); manageChange(changes, n, true); } c->Set(Nan::New("details").ToLocalChecked(), changesetToObject(changes)); return scope.Escape(o); } 复制代码
该函数中构造了两个对象b(before)、a(after)用于保存前后两个快照的详细信息。用一个js对象描述如下:
// b(before) / a(after) { nodes: // heap snapshot中对象节点个数 size_bytes: // heap snapshot的对象大小(bytes) size: // heap snapshot的对象大小(kb、mb) } 复制代码
进一步对前后两次的快照进行分析可以得到o,o中的before、after对象就是前后两次的snapshot对象的引用:
// o { before: { // before的堆snapshot nodes: size_bytes: size: }, after: { // after的堆snapshot nodes: size_bytes: size: }, change: { freed_nodes: // gc掉的节点数量 allocated_nodes: // 新增节点数量 details: [ // 按照类型String、Array聚合出来的详细信息 { Array : { what: // 类型 size_bytes: // 字节数bytes size: // kb、mb +: // 新增数量 -: // gc数量 } }, {} ] } } 复制代码
得到两次snapshot对比的结果后将o返回出去,在End函数中通过 info.GetReturnValue().Set(comparison);
将结果传递到js层面。
下面来具体说下compare函数中的buildIDSet、setDiff以及manageChange函数的实现。
buildIDSet的用法: buildIDSet(&beforeIDs, before->GetRoot(), s);
,该函数会从堆snapshot的根节点出发,递归的寻找所有能够访问的子节点,加入到集合seen中,做DFS统计所有可达节点的同时,也会对所有节点的shallowSize(对象本身占用的内存,不包括引用的对象所占内存)进行累加,统计当前堆所占用的内存大小。其具体实现如下:
static void buildIDSet(set<uint64_t> * seen, const HeapGraphNode* cur, int & s) { Nan::HandleScope scope; if (seen->find(cur->GetId()) != seen->end()) { return; } if (cur->GetType() == HeapGraphNode::kObject && handleToStr(cur->GetName()).compare("HeapDiff") == 0) { return; } s += cur->GetShallowSize(); seen->insert(cur->GetId()); for (int i=0; i < cur->GetChildrenCount(); i++) { buildIDSet(seen, cur->GetChild(i)->GetToNode(), s); } } 复制代码
setDiff函数用法: setDiff(beforeIDs, afterIDs, changedIDs);
主要用来计算集合差集用的,具体实现很简单,这里直接贴代码,不再赘述:
typedef set<uint64_t> idset; // why doesn't STL work? // XXX: improve this algorithm void setDiff(idset a, idset b, vector<uint64_t> &c) { for (idset::iterator i = a.begin(); i != a.end(); i++) { if (b.find(*i) == b.end()) c.push_back(*i); } } 复制代码
manageChange函数用法: manageChange(changes, n, false);
,其作用在于做数据的聚合。对某个指定的set,按照set中对象的类型,聚合出每种对象创建了多少、销毁了多少,实现如下:
static void manageChange(changeset & changes, const HeapGraphNode * node, bool added) { std::string type; switch(node->GetType()) { case HeapGraphNode::kArray: type.append("Array"); break; case HeapGraphNode::kString: type.append("String"); break; case HeapGraphNode::kObject: type.append(handleToStr(node->GetName())); break; case HeapGraphNode::kCode: type.append("Code"); break; case HeapGraphNode::kClosure: type.append("Closure"); break; case HeapGraphNode::kRegExp: type.append("RegExp"); break; case HeapGraphNode::kHeapNumber: type.append("Number"); break; case HeapGraphNode::kNative: type.append("Native"); break; case HeapGraphNode::kHidden: default: return; } if (changes.find(type) == changes.end()) { changes[type] = change(); } changeset::iterator i = changes.find(type); i->second.size += node->GetShallowSize() * (added ? 1 : -1); if (added) i->second.added++; else i->second.released++; return; } 复制代码
upon_gc
和 gc
实现
这两个方法的在init函数中声明如下:
Nan::SetMethod(target, "upon_gc", memwatch::upon_gc); Nan::SetMethod(target, "gc", memwatch::trigger_gc); 复制代码
先看gc方法的实现,实际上对应 memwatch::trigger_gc
,实现如下:
NAN_METHOD(memwatch::trigger_gc) { Nan::HandleScope scope; int deadline_in_ms = 500; if (info.Length() >= 1 && info[0]->IsNumber()) { deadline_in_ms = (int)(info[0]->Int32Value()); } Nan::IdleNotification(deadline_in_ms); Nan::LowMemoryNotification(); info.GetReturnValue().Set(Nan::Undefined()); } 复制代码
通过 Nan::IdleNotification
和 Nan::LowMemoryNotification
触发v8的gc功能。
再来看 upon_gc
方法,该方法实际上会绑定一个函数,当执行到gc方法时,就会触发该函数:
NAN_METHOD(memwatch::upon_gc) { Nan::HandleScope scope; if (info.Length() >= 1 && info[0]->IsFunction()) { uponGCCallback = new UponGCCallback(info[0].As<v8::Function>()); } info.GetReturnValue().Set(Nan::Undefined()); } 复制代码
其中info[0]就是用户传入的回调函数。调用new UponGCCallback的时候,其对应的构造函数内部会执行:
UponGCCallback(v8::Local<v8::Function> callback_) : Nan::AsyncResource("memwatch:upon_gc") { callback.Reset(callback_); } 复制代码
把用户传入的callback_函数设置到UponGCCallback类的成员变量callback上。upon_gc回调的触发与gc的钩子有关,详细看下一节分析。
gc前、后钩子函数的实现
gc钩子的挂载如下:
Nan::AddGCPrologueCallback(memwatch::before_gc); Nan::AddGCEpilogueCallback(memwatch::after_gc); 复制代码
先来看 memwatch::before_gc
函数的实现,内部给gc开始记录了时间:
NAN_GC_CALLBACK(memwatch::before_gc) { currentGCStartTime = uv_hrtime(); } 复制代码
再来看 memwatch::after_gc
函数的实现,内部会在gc后记录gc的结果到GCStats结构体中:
struct GCStats { // counts of different types of gc events size_t gcScavengeCount; // gc 扫描次数 uint64_t gcScavengeTime; // gc 扫描事件 size_t gcMarkSweepCompactCount; // gc标记清除整理的个数 uint64_t gcMarkSweepCompactTime; // gc标记清除整理的时间 size_t gcIncrementalMarkingCount; // gc增量标记的个数 uint64_t gcIncrementalMarkingTime; // gc增量标记的时间 size_t gcProcessWeakCallbacksCount; // gc处理weakcallback的个数 uint64_t gcProcessWeakCallbacksTime; // gc处理weakcallback的时间 }; 复制代码
对gc请求进行统计后,通过v8的api获取堆的使用情况,最终将结果保存到barton中,barton内部维护了一个uv_work_t的变量req,req的data字段指向barton对象本身。
NAN_GC_CALLBACK(memwatch::after_gc) { if (heapdiff::HeapDiff::InProgress()) return; uint64_t gcEnd = uv_hrtime(); uint64_t gcTime = gcEnd - currentGCStartTime; switch(type) { case kGCTypeScavenge: s_stats.gcScavengeCount++; s_stats.gcScavengeTime += gcTime; return; case kGCTypeMarkSweepCompact: case kGCTypeAll: break; } if (type == kGCTypeMarkSweepCompact) { s_stats.gcMarkSweepCompactCount++; s_stats.gcMarkSweepCompactTime += gcTime; Nan::HandleScope scope; Baton * baton = new Baton; v8::HeapStatistics hs; Nan::GetHeapStatistics(&hs); timeval tv; gettimeofday(&tv, NULL); baton->gc_ts = (tv.tv_sec * 1000000) + tv.tv_usec; baton->total_heap_size = hs.total_heap_size(); baton->total_heap_size_executable = hs.total_heap_size_executable(); baton->req.data = (void *) baton; uv_queue_work(uv_default_loop(), &(baton->req), noop_work_func, (uv_after_work_cb)AsyncMemwatchAfter); } } 复制代码
在前面工作完成的基础上,将结果丢到libuv的loop中,等到合适的实际触发回调函数,在回调函数中可以拿到req对象,通过访问req.data对其做强制类型装换可以得到barton对象,在loop的回调函数中,将barton中封装的数据依次取出来,保存到stats对象中,并调用uponGCCallback的Call方法,传入字面量 stats
和stats对象。
static void AsyncMemwatchAfter(uv_work_t* request) { Nan::HandleScope scope; Baton * b = (Baton *) request->data; // if there are any listeners, it's time to emit! if (uponGCCallback) { Local<Value> argv[2]; Local<Object> stats = Nan::New<v8::Object>(); stats->Set(Nan::New("gc_ts").ToLocalChecked(), javascriptNumber(b->gc_ts)); stats->Set(Nan::New("gcProcessWeakCallbacksCount").ToLocalChecked(), javascriptNumberSize(b->stats.gcProcessWeakCallbacksCount)); stats->Set(Nan::New("gcProcessWeakCallbacksTime").ToLocalChecked(), javascriptNumber(b->stats.gcProcessWeakCallbacksTime)); stats->Set(Nan::New("peak_malloced_memory").ToLocalChecked(), javascriptNumberSize(b->peak_malloced_memory)); stats->Set(Nan::New("gc_time").ToLocalChecked(), javascriptNumber(b->gc_time)); // the type of event to emit argv[0] = Nan::New("stats").ToLocalChecked(); argv[1] = stats; uponGCCallback->Call(2, argv); } delete b; } 复制代码
最后在Call函数的内部调用js传入的callback_函数,并将字面量 stats
和stats对象传递到js层面,供上层用户使用。
void Call(int argc, Local<v8::Value> argv[]) { v8::Isolate *isolate = v8::Isolate::GetCurrent(); runInAsyncScope(isolate->GetCurrentContext()->Global(), Nan::New(callback), argc, argv); } 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 从Zend虚拟机分析PHP加密扩展
- Dubbo源码分析(一)Dubbo的扩展点机制
- Nfstream:一款易于扩展的网络数据分析框架
- Apache Spark 实现可扩展日志分析,挖掘系统最大潜力
- Go语言经典库使用分析(七)| 高性能可扩展 HTTP 路由 httprouter
- TP-Link Wi-Fi扩展器远程代码执行漏洞分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java核心技术·卷1:基础知识(原书第9版)
(美)Cay S. Horstmann、(美)Gary Cornell / 周立新、陈波、叶乃文、邝劲筠、杜永萍 / 机械工业出版社 / 2013-11-1 / 119.00
Java领域最有影响力和价值的著作之一,拥有20多年教学与研究经验的资深Java技术专家撰写(获Jolt大奖),与《Java编程思想》齐名,10余年全球畅销不衰,广受好评。第9版根据JavaSE7全面更新,同时修正了第8版中的不足,系统全面讲解Java语言的核心概念、语法、重要特性和开发方法,包含大量案例,实践性强。 《Java核心技术·卷1:基础知识》共14章。第1章概述了Java语言与其......一起来看看 《Java核心技术·卷1:基础知识(原书第9版)》 这本书的介绍吧!