node.js内存泄露
Once we begin to type that code, we already introduce bugs and allocating memory without knowing it. How we manage them can make or mar our software.
ØNCE我们开始键入的代码,我们已经引入错误和分配内存而不自知。 我们如何管理它们可以制造或破坏我们的软件。
In this post, we will learn, using examples, about memory leaks in Nodejs and how we can solve them.
在这篇文章中,我们将通过示例学习有关Nodejs中的内存泄漏以及如何解决它们的信息。
In our effort to understand what a memory leak is, let’s first understand the memory model in JavaScript. This memory model visualizes how a JavaScript program is mapped in the RAM during execution.
为了了解什么是内存泄漏,首先让我们了解JavaScript中的内存模型。 该内存模型可视化执行期间JavaScript程序在RAM中的映射方式。
Let’s take a look.
让我们来看看。
记忆模型 (Memory Model)
JavaScript has three portions of memory assigned to a program during execution: Code Area, Call Stack, and Heap. These combined are known as the Address Space of the program.
JavaScript在执行期间为程序分配了三部分内存:代码区,调用堆栈和堆。 这些组合在一起被称为程序的地址空间。
Code Area: This is the area the JS code to be executed is stored.
代码区域:这是要执行的JS代码的存储区域。
Call Stack: This area keeps track of currently executing functions, perform computation, and store local variables. The variables are stored in the stack in a LIFO method. The last one in is the first out. Value data types are stored here.
调用堆栈:此区域跟踪当前执行的函数,执行计算并存储局部变量。 变量以LIFO方法存储在堆栈中。 最后一个是先进先出。 值数据类型存储在此处。
For example:
例如:
var corn = 95
let lion = 100
Here, corn and lion values are stored in the stack during execution.
在这里,玉米和狮子值在执行期间存储在堆栈中。
Heap: This is where JavaScript reference data types like objects are allocated. Unlike stack, memory allocation is randomly placed, with no LIFO policy. And to prevent memory “holes” in the Heap, the JS engine has memory managers that prevent them from occurring.
堆:这是分配JavaScript参考数据类型(如对象)的位置。 与堆栈不同,内存分配是随机放置的,没有LIFO策略。 为了防止堆中的内存“空洞”,JS引擎具有内存管理器,可以防止它们发生。
class Animal {}// stores `new Animal()` instance on memory address 0x001232
// tiger has 0x001232 as value in stack
const tiger = new Animal()// stores `new Object()` instance on memory address 0x000001
// `lion` has 0x000001 as value on stack
let lion = {
strength: "Very Strong"
}
Here, lion and tiger are reference types, their values are stored in the Heap and they are pushed to the stack. Their values in stack hold the memory address of the location in Heap.
在这里,狮子和老虎是参考类型,它们的值存储在堆中,并被压入堆栈。 它们在堆栈中的值保存堆中位置的内存地址。
Tip: Share your reusable components between projects using Bit (Github). Bit makes it simple to share, document, and organize independent components from any project.
提示:使用Bit ( Github )在项目之间共享可重用组件。 Bit使共享,记录和组织来自任何项目的独立组件变得简单。
Use it to maximize code reuse, collaborate on independent components, and build apps that scale.
使用它可以最大程度地重复使用代码,在独立组件上进行协作以及构建可扩展的应用程序。
Bit supports Node, TypeScript, React, Vue, Angular, and more.
Bit支持Node,TypeScript,React,Vue,Angular等。
JavaScript中的内存管理(Memory management in JavaScript)
Note: This applies to Nodejs because Node.js runs on a V8 JavaScript engine. So whatever happens in Nodejs happens in JavaScript.
注意:这适用于Node.js,因为Node.js在V8 JavaScript引擎上运行。 因此,Nodejs中发生的一切都在JavaScript中发生。
In JavaScript, memory is divided into two: Heap and Stack.
在JavaScript中,内存分为两种:堆和堆栈。
Stack: This is the area in our JS program address space where primitives values and pointers to object references to the heap are stored. The Stack is an organized memory space managed by the OS. It uses the FILO(First In Last Out) principle.
堆栈:这是我们JS程序地址空间中存储原始值和指向堆的对象引用的指针的区域。 堆栈是由操作系统管理的有组织的内存空间。 它使用FILO(先进先出)原理。
The Stack is just like a pile of books stacked from bottom to top. To get a book you must pop off from the top, and you can never take a book anywhere stack except the top. In this way, the stack is highly organized.
堆叠就像是一堆从下往上堆叠的书。 要获得一本书,您必须从顶部弹出,并且除了顶部之外,您永远都不能将书带到任何地方。 这样,堆栈是高度组织的。
Heap: This is the memory space where objects are stored. The objects are created here and their references are stored on the Stack.
堆:这是存储对象的内存空间。 在这里创建对象,并将它们的引用存储在堆栈中。
Heap is not orderly organized as the Stack. Memory spaces are allocated and deallocated randomly and without a pattern. It is up to the JS runtime memory manager to alloc and dealloc to prevent holes in the Heap and stale objects(object without reference in the Stack).
堆没有作为堆栈有序地组织。 内存空间是随机分配和释放的,没有模式。 JS运行时内存管理器负责分配和取消分配,以防止堆和陈旧对象(堆栈中没有引用的对象)出现漏洞。
The Garbage Collector in V8 is responsible for looking out for lost object references in the Heap and deallocating the space. It uses the Mark and Sweep algorithm to find and mark un-referenced objects and then it sweeps them out, which is deallocation. Let’s look at GC’s Mark and Sweep operation in more detail.
V8中的垃圾收集器负责在堆中查找丢失的对象引用并重新分配空间。 它使用“标记并清除”算法查找并标记未引用的对象,然后将它们清除出去,这就是释放。 让我们更详细地了解GC的“标记和扫描”操作。
GC:扫一扫 (GC: Mark and Sweep)
The Mark and Sweep operation start from the root of the application. For Nodejs, it is the global
object and for the browser, it is the window
object.
标记和扫描操作从应用程序的根目录开始。 对于Nodejs,它是global
对象,对于浏览器,它是window
对象。
All variables stored in these roots are global variables. These global variables are marked as always present and active.
这些根中存储的所有变量都是全局变量。 这些全局变量被标记为始终存在且处于活动状态。
Mark and Sweep have two cycles: Mark, and Sweep.
标记和扫描有两个周期:标记和扫描。
In the Mark cycle: Global variables are marked as active. The children of these global variables are recursively inspected and everything that can be referenced is marked as active.
在标记周期中:全局变量标记为活动。 递归检查这些全局变量的子代,并将所有可以引用的标记为活动。
In the Sweep cycle, GC collects all variables not marked as active and frees their memory space.
在扫描周期中,GC收集所有未标记为活动的变量,并释放其内存空间。
We now know JS have a GC that sweeps and frees dangling objects, let’s now see what a memory leak is and how it can occur.
现在我们知道JS具有一个可以清除并释放悬空对象的GC,现在让我们看看什么是内存泄漏以及如何发生。
内存泄漏 (Memory Leak)
A memory leak occurs when the developer declares a variable that points to a piece of memory and forgets about it. A memory leak can also occur when a variable is accidentally created in the wrong scope, this leaves the piece of memory dangling even after the intended scope is long GC’d.
当开发人员声明一个指向内存的变量并将其遗忘时,就会发生内存泄漏。 当在错误的范围内意外创建变量时,也会发生内存泄漏,即使在预期的范围经过长时间的GC操作后,这也会导致内存悬空。
So practically a memory leak occurs due to the developer’s fault, forgetting about a variable and wrong knowledge about scoping in JS. This all leads to a memory leak.
因此,实际上,由于开发人员的错误而导致内存泄漏,从而忘记了有关JS范围界定的变量和错误知识。 所有这些都会导致内存泄漏。
Let’s look at ways in which we can have a memory leak in Nodejs.
让我们看看在Nodejs中内存泄漏的方法。
范围 (Scope)
Scoping is one of the major causes of memory leak in Nodejs. Scoping in JS is generally tough to wrap your head around, even the highly seasoned devs sometimes declare variables in the wrong scope.
范围界定是Nodejs中内存泄漏的主要原因之一。 用JS进行作用域通常很难扎根,即使是经验丰富的开发人员有时也会在错误的范围内声明变量。
Memory leak in JS occur due to scoping rules. Let’s say we have this code:
JS中的内存泄漏是由于作用域规则而发生的。 假设我们有以下代码:
function aFunc() {
foo = 900
}
A quick look at this code, we may think that the foo variable is created inside the scope of this aFunc function. But no, the foo variable is created at the global scope(that is Hoisting). So when the aFunc scope is destroyed after being called, the foo still exists in the global scope in the global
variable. This is a memory leak.
快速浏览此代码,我们可能会认为foo变量是在此aFunc函数的范围内创建的。 但是不,foo变量是在全局范围内创建的(即提升)。 因此,当aFunc作用域在调用后销毁时,foo仍然存在于global
变量的全局作用域中。 这是内存泄漏。
This memory leak can introduce problems. If we have a global variable or global function with name foo. Then, this memory leak will cause the global foo to be overwritten by the foo in the aFunc.
此内存泄漏可能会带来问题。 如果我们有一个名称为foo的全局变量或全局函数。 然后,此内存泄漏将导致全局foo被aFunc中的foo覆盖。
We now see that scoping variables wrongly can lead to some serious errors in our code.
现在我们看到错误地对变量进行范围界定会导致我们的代码中出现一些严重的错误。
Even if another foo global variable exists, the foo in the aFunc will be left dangling.
即使存在另一个foo全局变量,aFunc中的foo也会悬空。
To mitigate all these leaks, variables intended to exist inside functions, or objects are to be clearly defined with the keyword var
, const
let
. These make them to be created inside the function's or object's scope, so when the scope is cleared the variables inside them are GC'd (garbage collected).
为了减轻所有这些泄漏,应使用关键字var
, const
let
明确定义要存在于函数或对象内部的变量。 这些使它们可以在函数或对象的作用域内创建,因此,清除作用域后,它们内的变量将被GC'd(收集垃圾)。
全局变量 (Global Variable)
We learned that in JS’s GC MArk-and-Sweep algorithm that global variables are never collected as they are always marked as active. This poses a huge risk of memory risk if the dev assigns global variables and forgets about them i.e and never uses them. That blocks of memory would be uselessly occupied, and will eventually bloat up the memory space of the entire making the program eat up our machine resources and eventually slow down the program.
我们了解到,在JS的GC MArk-and-Sweep算法中,永远不会收集全局变量,因为它们始终被标记为活动变量。 如果开发人员分配了全局变量而忘记了全局变量(即从不使用它们),则会带来巨大的内存风险。 这些内存块将被无用地占用,最终将占用整个内存空间,使程序吞噬我们的机器资源并最终使程序变慢。
var aGlobalVar = 900
var anotherGloablVar = 9000
var aBigGlobalVar = new Array(1000)
var aBigVeryGlobalVar = new Array(1000000)
We have four global variables there. See the aBigGlobalVar
and its brother, aVeryBigGlobalVar
consumes a lot of memory space. aGlobalVar and anotherGlobalVar do not consume as much as their colleagues. aBigGlobalVar sets up an array with 1000 space for elements, aVeryBigGlobalVar sets a million spaces in its array.
我们在那里有四个全局变量。 请参阅aBigGlobalVar
及其兄弟aVeryBigGlobalVar
占用大量内存空间。 aGlobalVar和另一个GlobalVar的消耗不及其同事。 aBigGlobalVar设置一个具有1000个元素空间的数组,aVeryBigGlobalVar设置其数组中的一百万个空格。
If the aBigGlobalVar, and aVeryBigGlobalVar are never used and forgotten, that will mean a (1000000000) memory space size are completely occupied and unused. They are never garbage collected because they are global variables.
如果aBigGlobalVar和aVeryBigGlobalVar从未被使用和遗忘,则意味着(1000000000)的内存空间大小已被完全占用和未使用。 因为它们是全局变量,所以它们永远不会被垃圾回收。
One way to solve this problem is to nullify your global variables after usage by using the null
keyword. This will free up the memory space occupied by the variable thus, making your program a lesser RAM-eater.
解决此问题的一种方法是在使用后通过使用null
关键字使全局变量null
。 这样可以释放变量占用的内存空间,从而使程序成为较少的RAM占用者。
Others are:
其他是:
- Use global variables sparingly 谨慎使用全局变量
- Don’t store heavy values in global variables不要在全局变量中存储重值
- Always remember to clean out your no-longer-wanted global variables.永远记得清除不再需要的全局变量。
快取 (Caching)
Caching is one of the ways we can leak memory in JS.
缓存是我们可以在JS中泄漏内存的方法之一。
Caching comes in very handy when we want to speed up our program especially programs that involve huge mathematical computations that involve a lot of CPU power. It prevents the re-calculation of values, especially pure value. Now, caching speeds up computing power at the expense of memory. It stores computed values in-memory and return them immediately on demand without recalculating them.
当我们想加快程序速度时,特别是那些涉及大量数学计算且需要大量CPU能力的程序时,缓存非常方便。 它可以防止重新计算值,尤其是纯值。 现在,缓存以牺牲内存为代价来提高计算能力。 它将计算的值存储在内存中,并根据需要立即返回它们,而无需重新计算它们。
This in-memory caching can expand out of hand, and introduce memory leaks if the stored values are never used. They will sit there dormant taking up precious space because their content cannot be collected.
如果从不使用存储的值,则此内存中缓存可能会失控,并导致内存泄漏。 他们将坐在那里Hibernate,占用宝贵的空间,因为它们的内容无法收集。
The best way to mitigate the risk of memory leak in caching is to constantly do away with the never-used values in the cache.
减轻缓存中内存泄漏风险的最佳方法是不断消除缓存中从未使用的值。
DOM参考 (DOM references)
You may never know but using DOM references also creates memory leak. We can refer to an element DOM reference using any of the DOM APIs:
您可能永远不会知道,但是使用DOM引用也会造成内存泄漏。 我们可以使用任何DOM API来引用元素DOM参考:
- getElementById getElementById
- getElementbyClassName getElementbyClassName
- getElementByTagName getElementByTagName
These APIs retrieves the DOM reference of the specified element and is stored in a variable. If the element is eventually destroyed or removed from the DOM, the removed element’s DOM reference is being kept alive in the variable it was stored earlier. In other words, the element was destroyed but yet it still lives in memory.
这些API检索指定元素的DOM引用,并将其存储在变量中。 如果该元素最终被销毁或从DOM中删除,则删除的元素的DOM引用将保留在先前存储的变量中。 换句话说,该元素已被破坏,但仍保留在内存中。
<body>
<p id="para1">P1</p>
<p id="para2">P2</p>
</body>// DOM reference of "p" element with "id" "para1" is stored in the p1 variable
var p1 = document.getElementById("para1")// Let's remove "para1" from DOM
document.body.removeChild(p1)"para1" is removed from DOM but yet still exists in memory.// Test
p1.innerText
P1p1
<p id="para1">
See? a block of memory is still occupied despite being removed from the DOM. Some developers will still think that the “para1” element is gone. We can still even append the “para1” element back to the body using the p1 variable:
看到? 尽管已从DOM中删除了一块内存,但仍然占用了一块内存。 一些开发人员仍然会认为“ para1”元素已消失。 我们甚至可以使用p1变量将“ para1”元素附加回正文:
document.body.appendChild(p1)
This example might not have a huge impact, but imagine removing a deep nested DOM and still having its DOM reference in memory. We will have precious large memory space occupied by the DOM reference and will cause the OS to create more memory for application leaving us with a large memory footprint.
该示例可能不会产生很大的影响,但请想象一下,删除一个深层嵌套的DOM并将其DOM引用保留在内存中。 我们将拥有DOM引用占用的宝贵大内存空间,并且将导致OS为应用程序创建更多内存,从而使我们的内存占用量很大。
One way to solve this issue is to delete or nullify DOM references kept in variables, this way the memory they occupied will be freed, giving our program with a small memory footprint.
解决此问题的一种方法是删除或使变量中保留的DOM引用无效,这样可以释放它们所占用的内存,从而为我们的程序提供较小的内存占用。
事件监听器 (Event listeners)
Event listeners like “onclick”, etc can introduce memory leak in our application. Event listeners mostly have impure function handlers, they always depend on variables on their parent scope. This dependency on their parent scope variables makes these variables to be marked as active by GC and never collected.
诸如“ onclick”之类的事件监听器会在我们的应用程序中引入内存泄漏。 事件侦听器大多具有不纯函数处理程序,它们始终依赖于其父作用域上的变量。 对它们的父作用域变量的这种依赖性使这些变量被GC标记为活动的,并且从未收集。
Example:
例:
var aGlobalVar = 900var onClick = (evt) => {
var result = aGlobarVar * Date.now()
}buttonElement.addEventListener("click", onClick)
See, this “click” event handler is dependant on the aGlobalVar variable. This aGlobalVar will never be collected because an event listener uses it. So on the GC’s Mark-and-Sweep cycle, the variables live. This becomes a memory leak if the “click” event is readily registered on the button element but never used, or completely forgotten. This will make the aGlobalVar variable occupy precious memory space.
可见,此“ click”事件处理程序取决于aGlobalVar变量。 永远不会收集此aGlobalVar,因为事件侦听器会使用它。 因此,在GC的“标记和扫描”周期中,变量存在。 如果“单击”事件很容易在按钮元素上注册但从未使用或完全被遗忘,则这将导致内存泄漏。 这将使aGlobalVar变量占用宝贵的内存空间。
One way to avoid this is to clean up our event listeners when no longer in need of it.
避免这种情况的一种方法是在不再需要事件监听器时对其进行清理。
buttonElement.removeEventListener("click", onClick)
This will free the aGlobarVar and GC can collect it.
这将释放aGlobarVar,GC可以收集它。
关闭 (Closures)
Closures are the harbinger of memory leaks. Its nature makes it so. Closures are so smart that they can remember the variables kept in its parent scope and import it in its scope making them usable. That’s clever. But this parent scope isn’t kept in the space they are in the memory.
关闭是内存泄漏的预兆。 它的本性使其成为现实。 闭包是如此聪明,以至于他们可以记住保留在其父范围内的变量,并将其导入其范围内以使其可用。 那很聪明。 但是此父作用域没有保留在它们在内存中的空间中。
I would say things don’t die quickly in JavaScript. Kill it and it’s still alive in memory.
我会说事情不会在JavaScript中很快消失。 杀死它,它仍然存在于内存中。
When the parent scope of a closure is removed, the closure retains a copy of it in memory so callers can still refer the old variables from the closure’s parent scope.
删除闭包的父范围后,闭包会将其副本保留在内存中,因此调用者仍可以从闭包的父范围中引用旧变量。
Example:
例:
function noClosure() {
var cFoo = 900
function closureFunc() {
return cFoo
}
return closureFunc
}var closure = noClosure()
cloure()// 900
The cFoo will still be kept in memory by closureFunc and returned.
cFoo仍将由closureFunc保留在内存中并返回。
Now, what happens if the parent scope closures are not used or some are not used?
现在,如果不使用父范围闭包或不使用某些父范围闭包,会发生什么?
function noClosure() {
var cFoo = 900
var cBaz = new Array(100000)
function closureFunc() {
return cFoo
}
return closureFunc
}var closure = noClosure()
cloure()// 900
See we have two closure variables cFoo and CBaz that would still exist in closureFunc scope. Only the cFoo is used and the big ol’ cBaz si kept in memory! That’s a huge memory leak.
看到我们有两个闭包变量cFoo和CBaz仍将存在于closureFunc范围内。 仅使用cFoo,并且将大笔cBaz si保留在内存中! 那是一个巨大的内存泄漏。
Also, every time a new closure is created a 100000 memory space is allocated. This will result in a fast dwindling of resources allocated to our process and would eventually slow down execution because the OS would struggle to allocate more memory to our program.
另外,每次创建新的闭包时,都会分配100000个存储空间。 这将导致分配给我们进程的资源快速减少,并最终会降低执行速度,因为OS难以为我们的程序分配更多的内存。
We should be careful about how we create closures so as to disallow memory leaks.
我们应该谨慎考虑如何创建闭包,以防止内存泄漏。
计时器 (Timers)
Timers such as setInterval, setTimeout are surprisingly another source of memory leak in JavaScript.
令人惊讶的是,诸如setInterval,setTimeout之类的计时器是JavaScript中内存泄漏的另一个来源。
Uncleared timers can run forever and would seize resources that should have been collected.
未清除的计时器可能永远运行,并且会占用本应收集的资源。
See this code:
参见以下代码:
var aGlobalVar = 900setInterval(() => {
aGlobalVar + Date.now()
...
}, 1000)
We have a timer set to run every 1ms. It depends on the aGlobalVar variable. Now, this variable can’t be garbage collected because it is dependant on a timer. This will introduce a memory leak if the timer is forgotten and remains uncleared, the timer will run forever and aGlobalVar would never be collected.
我们将计时器设置为每1ms运行一次。 它取决于aGlobalVar变量。 现在,该变量不能被垃圾回收,因为它依赖于计时器。 如果忘记了计时器并且不清除计时器,这将导致内存泄漏,计时器将永远运行并且永远不会收集GlobalVar。
A solution to this is to clear the timer with clearInterval API.
解决方案是使用clearInterval API清除计时器。
var aGlobalVar = 900var timer = setInterval(() => {
aGlobalVar + Date.now()
...
}, 1000)// We are done, then clear the "timer"
clearInterval(timer)
This will free the aGlobaVar and can now be GC’d. If setTimeout is used, we will use clearTimeout to clear out the timer.
这将释放aGlobaVar,现在可以进行GC处理了。 如果使用setTimeout,我们将使用clearTimeout清除计时器。
Note: setTimeout runs once on the specified time and doesn’t run again unlike setInterval, but its handler still remains active and in memory, so its outside dependencies are not released for GC. Using clearTimeout will deactivate and clear the setTimeout handler and free its dependencies.
注意:setTimeout在指定的时间运行一次,并且不会像setInterval一样再次运行,但是它的处理程序仍然保持活动状态并在内存中,因此不会将其外部依赖项释放给GC。 使用clearTimeout将停用并清除setTimeout处理程序并释放其依赖项。
结论 (Conclusion)
Memory leaks are not to be taken lightly. No user wants RAM-eaters programs and would easily discard a program when one program slows down his machine. So we must take great measures and care to make sure we don’t up the RAM limit in our user’s machine which this post gives.
内存泄漏不应掉以轻心。 没有用户想要吃RAM的程序,并且当一个程序降低计算机速度时,它很容易丢弃该程序。 因此,我们必须采取很大的措施并谨慎对待,以确保我们不会超出本帖子提供的用户计算机中的RAM限制。
If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email, or, DM me.
如果您对此有任何疑问,或者我应该添加,更正或删除任何内容,请随时发表评论,发送电子邮件或给我DM。
学到更多 (Learn More)
翻译自: https://blog.bitsrc.io/memory-leaks-in-nodejs-54ac7bbd4173
node.js内存泄露