有那么一句名言,过早优化是万恶之源。指的就是在开发过程中,不用太早考虑性能问题,而是要优先实现功能和保持代码清晰,简洁。更深的原因,是对个人的判断的极端不信任。我们觉得重要的往往不重要,我们觉得不重要的事后才发现很重要。事前的判断,往往会错得离谱。所以,在软件工程中,就放弃了事前对可能的性能占用情况的评估,而是在实现运行中,通过软件工具,来分析性能瓶颈,即:profiling.
我最近就遇到一个profiling的情况。
最初是因为有那么一个bug。导致js后台处理进程,一小时左右,会自动退出。经过分析,我发现这是我所使用的kafka连接库产生的问题,这个问题不好修复,但是我可以使用trick绕过它。处理之后,进程不会报错退出了,但是,在运行3-4小时后,进程的处理性能会出现巨大的下降,而且内存占用会不停的增长,不停的刷新占用的上限。
很明显,这个js进程存在内存泄漏的情况。
但是内存泄漏是哪里产生的咧?
由于之前没有做过profiling,无法判断是旧有的代码就有问题,还是新加入的代码导致的。所以只能全局的去考虑。
最开始,我使用了nodejs自带的profiling工具进行过一些cpu性能分析
https://nodejs.org/en/docs/guides/simple-profiling/
后来我又找到了更直观的node-inspector
https://github.com/node-inspector/node-inspector
通过node-inspector分析,所能了解的也不多。只是看到closure,也就是闭包,有占到26%的内存,是最大的一项。
这么说,是我代码中的closure导致的内存泄漏。我又搜索了相关资料,了解了一下closure导致内存泄漏的常见形式。给我启发最大的是这么一篇:
http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html
在里面提到js v8一个惊人的细节。在同一作用域定义的闭包,会共享上下文(context),经典的触发代码如下:
var str = new Array(1000000).join('*');
var doSomethingWithStr = function () {
if (str === 'something')
console.log("str was something");
};
doSomethingWithStr();
var logIt = function () {
console.log('interval');
}
setInterval(logIt, 100);
};
setInterval(run, 1000);
在这段代码中logIt和doSomethingWithStr共享上下文,引用了str,导致str不被会gc清理,从而内存泄漏。
我又看了一下代码,代码中并没有使用setInteval函数,但使用的Rxjs,Rxjs的Observable.subscribe函数会不会有和setInteval函数类似的效果。我立即写了段代码测试了一下。
var subscription = null
var sequence = null
function run() {
var str = new Array(1000000).join('*');
if (subscription != null) {
subscription.dispose()
}
var sequence = Rx.Observable.interval(200)
var subscription = sequence.subscribe(
function() {
console.log("str[0]=", str[0])
})
}
setInterval(run, 500);
使用工具查看,发现这段代码会导致内存占用迅速上升,即存在内存泄漏。
经过试验后,我找到了初步的解决方案。
var subscription = null
var sequence = null
function run() {
var str = new Array(1000000).join('*');
if (subscription != null) {
subscription.dispose()
}
var sequence = Rx.Observable.interval(200).take(3)
var subscription = sequence.subscribe(
function() {
console.log("str[0]=", str[0])
} , function(err) {
console.log("err=", err)
} , function(err) {
str=null
console.log("completed")
} )
}
setInterval(run, 500);
即在消息流结束的时候,将str对象置为null,来防止str对象的泄漏。本以为这就是最佳方案。无意中我又发现,哪怕没有在onCompleted函数,只要流结束,也不会内存泄漏。
我觉得这个rxjs的一个bug。在dispose后,不就是应该该把要清理都清理了,是吧?