Home >Web Front-end >JS Tutorial >Introduction to JavaScript memory management + how to deal with 4 common memory leaks
We will Another important topic discussed is memory management, which is easily overlooked by developers due to the increasing maturity and complexity of programming languages used every day. We will also provide some tips on how to deal with memory leaks in JavaScript. Following these tips in SessionStack will ensure that SessionStack does not cause memory leaks and does not increase the memory consumption of our integrated web applications.##Related free learning recommendations: javascript(Video)
If you want to read more high-quality articles, please click on the GitHub blog. Hundreds of high-quality articles are waiting for you every year!
OverviewProgramming languages like C have low-level memory management primitives such as malloc() and free(). Developers use these primitives to explicitly allocate and free memory from the operating system. JavaScript will allocate memory for objects (objects, strings, etc.) when they are created, and will "automatically" release the memory when they are no longer used. This process is called garbage collection. This seemingly "automatic" release of resources is a source of confusion because it gives JavaScript (and other high-level language) developers the false impression that they can care less about memory management. This is the idea. A big mistake. Even when working with high-level languages, developers should understand memory management (or at least know the basics). Sometimes, there are some problems with automatic memory management (such as bugs in the garbage collector or implementation limitations, etc.) and developers must understand these problems so that they can be handled correctly (or find an appropriate solution that can be maintained with minimal effort) code). Memory life cycleNo matter which programming language is used, the memory life cycle is the same: Here is a brief introduction Let’s look at each stage in the memory life cycle:This code shows the memory size occupied by integer and double-precision floating-point variables. But about 20 years ago, integer variables usually took up 2 bytes, while double-precision floating-point variables took up 4 bytes. Your code should not depend on the size of the current primitive data type.
The compiler will insert code that interacts with the operating system and allocate the number of stack bytes required to store variables.
In the above example, the compiler knows the exact memory address of each variable. In fact, whenever we write to the variable n
, it will be converted internally into information like "Memory address 4127963"
.
Note that if we try to access x[4]
, the data associated with m will be accessed. This is because accessing a non-existent element in the array (which is 4 bytes larger than the last actually allocated element in the array, x[3]), may end up reading (or overwriting) some m bits. This will definitely have unpredictable results on the rest of the program.
When functions call other functions, each function gets its own block on the call stack. It holds all local variables, but also has a program counter to remember where it is during execution. When the function completes, its memory block is used again elsewhere.
Unfortunately, things get a little complicated when you don't know at compile time how much memory a variable requires. Suppose we want to do the following:
At compile time, the compiler does not know how much memory the array needs to use, because this is determined by the value provided by the user.
Therefore, it cannot allocate space for variables on the stack. Instead, our program needs to explicitly request the appropriate space from the operating system at runtime. This memory is allocated from the heap space. The difference between static memory allocation and dynamic memory allocation is summarized in the following table:
Static memory allocation | Dynamic memory allocation |
---|---|
Size must be known at compile time | Size does not need to be known at compile time |
Executed at compile time | Executed at runtime |
Assigned to the stack | Assigned to the heap |
FILO (first in, last out) | No specific Allocation order |
To fully understand how dynamic memory allocation works, you need to spend more time on pointers, which may deviate too much from the topic of this article. I will not introduce the relevant knowledge of pointers in detail here.
Now the first step will be explained: how to allocate memory in JavaScript.
JavaScript relieves developers from the responsibility of manually handling memory allocation - JavaScript allocates memory and declares values by itself.
Certain function calls also result in memory allocation of objects:
Methods can allocate new values or objects :
Using allocated memory in JavaScript means reading and writing in it, this can be done by reading or writing variables Or the value of an object property, or by passing parameters to a function.
Most memory management problems occur at this stage
The most difficult part here is determining when the allocation is no longer needed of memory, it usually requires the developer to identify where in the program the memory is no longer needed and free it.
High-level languages embedd a mechanism called a garbage collector, whose job is to track memory allocation and usage in order to detect any time a piece of allocated memory is no longer needed. In this case, it will automatically release this memory.
Unfortunately, this process only makes a rough estimate, because it is difficult to know whether a certain piece of memory is really needed (it cannot be solved algorithmically).
Most garbage collectors work by collecting memory that is no longer accessed, i.e. all variables pointing to it have gone out of scope. However, this is an underestimation of the set of memory space that can be collected, because at any point in a memory location, there may still be a variable in scope pointing to it, but it will never be accessed again.
Because it is impossible to determine whether some memory is really useful, the garbage collector thought of a way to solve this problem. This section explains and understands the main garbage collection algorithms and their limitations.
The garbage collection algorithm mainly relies on references.
In the context of memory management, an object is said to reference another object if it has access rights to another object (either implicitly or explicitly). For example, a JavaScript object has references to its prototype (implicit reference) and property values (explicit reference).
In this context, the concept of "object" is extended to a broader scope than regular JavaScript objects, and also includes function scope (or global lexical scope).
Lexical scope defines how variable names are resolved within nested functions: inner functions contain the effects of the parent function even if the parent function has returned
This is the simplest garbage collection algorithm. If there is no reference to the object, the object is considered "garbage collectable", as shown in the following code:
When it comes to When looping, there is a limit. In the example below, two objects are created that reference each other, creating a loop. After the function call will go out of scope, so they are effectively useless and can be released. However, the reference counting algorithm assumes that since each object is referenced at least once, none of them can be garbage collected.
This algorithm can determine whether an object can be accessed, thereby knowing whether the object is useful , the algorithm consists of the following steps:
#This algorithm is better than the previous algorithm, because "an object is not referenced" means that the object cannot be accessed.
As of 2012, all modern browsers have mark-sweeping garbage collectors. All the improvements made in the field of JavaScript garbage collection (generational/incremental/concurrent/parallel garbage collection) over the past few years have been implementation improvements to the algorithm (mark-sweep), not improvements to the garbage collection algorithm itself, Nor is it the goal of determining whether an object is accessible.
In this article, you can read in more detail about tracking garbage collection, including the mark-sweep algorithm and its optimizations.
In the first example above, after the function call returns, the two objects are no longer referenced by objects accessible from the global object. Therefore, the garbage collector will find them inaccessible.
Although there are references between objects, they are not reachable from the root node.
Although garbage collectors are very convenient, they have their own set of trade-offs, one of which is non-determinism. In other words, GC is not Predictably, you can't really tell when garbage collection will occur. This means that in some cases, the program will use more memory than is actually necessary. In particularly speed-sensitive applications, short pauses may be noticeable. If no memory is allocated, most of the GC will be idle. Take a look at the following scenario:
In these scenarios, most GCs will not proceed with collections. In other words, even if there are inaccessible references available for collection, the collector will not declare them. These are not strictly leaks, but can still result in higher than usual memory usage.
Essentially, a memory leak can be defined as: memory that is no longer needed by the application and, for some reason, does not return to the operation system or free memory pool.
Programming languages support different memory management methods. However, whether to use a certain piece of memory is actually an undetermined question. In other words, only the developer can tell whether a piece of memory can be returned to the operating system.
Some programming languages provide developers with assistance, others expect developers to have a clear understanding of when memory is no longer in use. Wikipedia has some great articles on manual and automatic memory management.
JavaScript handles undeclared variables in an interesting way: For undeclared variable, a new variable will be created in the global scope to reference it. In a browser, the global object is window. For example:
function foo(arg) { bar = "some text"; }
is equivalent to:
function foo(arg) { window.bar = "some text"; }
If bar refers to a variable within the scope of the foo function but forgets to use var to declare it, an unexpected global will be created. variable. In this example, missing a simple string wouldn't do much harm, but it would certainly be bad.
Another way to create an unexpected global variable is to use this:
function foo() { this.var1 = "potential accidental global"; } // Foo自己调用,它指向全局对象(window),而不是未定义。 foo();
You can avoid this by adding "use strict" at the beginning of the JavaScript file, which will turn on A stricter JavaScript parsing mode to prevent accidental creation of global variables.
Although we are talking about unknown global variables, there is still a lot of code filled with explicit global variables. By definition, these are not collectible (unless specified as empty or reallocated). Global variables used to temporarily store and process large amounts of information are of particular concern. If you must use a global variable to store a large amount of data, be sure to specify null or reassign it when you are done.
Take setInterval
as an example because it is often used in JavaScript.
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //每五秒会执行一次
The code snippet above demonstrates using a timer to reference nodes or data that are no longer needed.
The object represented by the renderer may be deleted at some point in the future, causing an entire block of code in the internal handler to become no longer needed. However, because the timer is still active, the handler cannot be collected, and its dependencies cannot be collected. This means that serverData, which stores large amounts of data, cannot be collected.
When using observers, you need to ensure that you make an explicit call to delete them after you are done using them (either the observer is no longer needed, or the object will become inaccessible).
As a developer, you need to make sure that you explicitly delete them when you are done with them (or the object will be inaccessible).
In the past, some browsers couldn't handle these situations (well IE6). Fortunately, most modern browsers now do this for you: they automatically collect observer handlers once the observed object becomes inaccessible, even if you forget to remove the listener. However, we should still explicitly remove these observers before the object is disposed. For example:
Today’s browsers (including IE and Edge) use modern garbage collection algorithms that can detect and handle these circular references immediately. In other words, it is not necessary to call removeEventListener before a node is deleted.
Some frameworks or libraries, such as JQuery, will automatically remove listeners before disposing of nodes (when using their specific API). This is implemented by a mechanism within the library that ensures that no memory leaks occur, even when running in problematic browsers, such as... IE 6.
Closures are a key aspect of JavaScript development. An inner function uses variables from an outer (enclosed) function. Due to the details of how JavaScript runs, it can cause a memory leak in the following way:
This code does one thing: every time replaceThing
is called , theThing
will get a new object containing a large array and a new closure (someMethod). At the same time, the variable unuse
d points to a closure that references `originalThing
.
Confused? The important thing is that once a scope with multiple closures of the same parent scope is created, this scope can be shared.
In this case, the scope created for the closure someMethod
can be unused
shared. unused
There is an internal reference to originalThing
. Even if unused
is never used, someMethod
can be passed outside the scope of replaceThing
(e.g. in the global scope) by theThing
to be called.
Since someMethod
shares the scope of the unused
closure, then the unused
reference to the included originalThing
will force it Keep alive (the entire shared scope between the two closures). This prevents it from being collected.
When this code is run repeatedly, you can observe that the memory usage is increasing steadily. When GC
is run, the memory usage will not become smaller. Essentially, a linked list of closures is created during runtime (its root exists in the form of the variable theThing
), and the scope of each closure indirectly references a large array. ,This caused a considerable memory leak.
Sometimes, it can be useful to store DOM nodes in a data structure. Suppose you want to quickly update several rows in a table, then you can save a reference to each DOM row in a dictionary or array. In this way, there are two references to the same DOM element: one in the DOM tree and the other in the dictionary. If at some point in the future you decide to delete these rows, you will need to make both references inaccessible.
There is another issue to consider when referencing internal nodes or leaf nodes in the DOM tree. If you keep a reference to a table cell (
You might think that the garbage collector will free everything except that cell. However, this is not the case, since the cell is a child node of the table, and child nodes hold references to parent nodes, this reference to the table cell will keep the entire table in memory, so When removing a referenced node, remove its child nodes.
The above is the detailed content of Introduction to JavaScript memory management + how to deal with 4 common memory leaks. For more information, please follow other related articles on the PHP Chinese website!