Compared with C/C++, the processing of JavaScript in memory in the JavaScript we use has made us pay more attention to the writing of business logic in development. However, with the continuous complexity of the business, the development of single-page applications, mobile HTML5 applications, Node.js programs, etc., the lag and memory overflow caused by memory problems in JavaScript have become no longer unfamiliar.
This article will discuss memory usage and optimization from the language level of JavaScript. From the aspects that everyone is familiar with or a little heard, to the things that everyone will not notice most of the time, we will analyze them one by one.
1. Language-level memory management
1.1 Scope
Scope is a very important operating mechanism in JavaScript programming. In synchronous JavaScript programming, it does not attract the attention of beginners, but in asynchronous programming, good scope control skills have become a necessary skill for JavaScript developers. In addition, scope plays a crucial role in JavaScript memory management.
In JavaScript, functions can be called, with statements and global scopes that can be scoped.
As shown in the following code as an example:
The code copy is as follows:
var foo = function() {
var local = {};
};
foo();
console.log(local); //=> undefined
var bar = function() {
local = {};
};
bar();
console.log(local); //=> {}
Here we define the foo() function and the bar() function. Their intention is to define a variable named local. But the final result is completely different.
In the foo() function, we use the var statement to declare that a local variable is defined. Because a scope is formed inside the function body, this variable is defined in the scope. Moreover, there is no scope extension processing in the foo() function, so after the function is executed, the local variable is also destroyed. However, this variable cannot be accessed in the outer scope.
In the bar() function, the local variable is not declared using a var statement, but instead it directly defines local as a global variable. Therefore, this variable can be accessed by the outer scope.
The code copy is as follows:
local = {};
// The definition here is equivalent to
global.local = {};
1.2 Scope Chain
In JavaScript programming, you will definitely encounter multi-layer function nesting scenarios, which is a typical representation of scope chains.
As shown in the following code:
The code copy is as follows:
function foo() {
var val = 'hello';
function bar() {
function baz() {
global.val = 'world;'
}
baz();
console.log(val); //=> hello
}
bar();
}
foo();
Based on the previous explanation about scope, you may think that the result shown in the code here is world, but the actual result is hello. Many beginners will start to feel confused here, so let’s take a look at how this code works.
Since JavaScript, the search for variable identifiers starts from the current scope and looks outward until the global scope. Therefore, access to variables in JavaScript code can only be performed outwards, but not in reverse.
The execution of the baz() function defines a global variable val in the global scope. In the bar() function, when accessing the identifier val, the principle of searching from the inside to the outside: if it is not found in the scope of the bar function, it goes to the previous layer, that is, the scope of the foo() function.
However, the key to confusing everyone is: this identifier access finds a matching variable in the scope of the foo() function, and will not continue to look outward, so the global variable val defined in the baz() function has no effect in this variable access.
1.3 Closure
We know that identifier lookup in JavaScript follows the inside-out principle. However, with the complexity of business logic, a single delivery order is far from meeting the increasing new needs.
Let's take a look at the following code:
The code copy is as follows:
function foo() {
var local = 'Hello';
return function() {
return local;
};
}
var bar = foo();
console.log(bar()); //=> Hello
The technology that allows outer scope to access inner scope as shown here is Closure. Thanks to the application of higher-order functions, the scope of the foo() function is "extended".
The foo() function returns an anonymous function, which exists within the scope of the foo() function, so you can access the local variable within the scope of the foo() function and save its reference. Because this function directly returns the local variable, the bar() function can be directly executed in the outer scope to obtain the local variable.
Closures are a high-level feature of JavaScript, and we can use them to achieve more and more complex effects to meet different needs. However, it should be noted that because the function with internal variable reference is brought out of the function, the variables in this scope will not be destroyed after the function is executed until all references to the internal variables are cancelled. Therefore, the application of closures can easily cause memory to be unfree.
2. JavaScript's memory recycling mechanism
Here I will take the V8 engine used by Chrome and Node.js and launched by Google as an example to briefly introduce the memory recycling mechanism of JavaScript. For more detailed content, you can purchase my good friend Park Ling’s book "In-depth and easy to understand Node.js" for learning, which is a very detailed introduction in the chapter "Memory Control".
In V8, all JavaScript objects are allocated through the "heap".
When we declare and assign values in the code, V8 will allocate a part of this variable in heap memory. If the requested memory is insufficient to store this variable, V8 will continue to apply for memory until the heap size reaches the memory limit of V8. By default, the upper limit of the heap memory of V8 is 1464MB on 64-bit systems and 732MB on 32-bit systems, which is about 1.4GB and 0.7GB.
In addition, V8 manages JavaScript objects in heap memory in generations: the new generation and the old generation. The new generation is JavaScript objects with short survival cycles, such as temporary variables, strings, etc.; while the old generation is objects with long survival cycles after multiple garbage collections, such as main controllers, server objects, etc.
Garbage recycling algorithms have always been an important part of the development of programming languages, and the garbage recycling algorithms used in V8 are mainly as follows:
1. Scavange algorithm: memory space management is performed through copying, mainly used in the memory space of the new generation;
2.Mark-Sweep algorithm and Mark-Compact algorithm: Heap memory is sorted and recycled through marking, mainly used for inspection and recycling of old-generation objects.
PS: More detailed V8 garbage collection implementations can be learned by reading related books, documents and source code.
Let's take a look at what circumstances will the JavaScript engine recycle what objects.
2.1 Scope and reference
Beginners often mistakenly believe that when the function is executed, the object declared inside the function will be destroyed. But in fact, this understanding is not rigorous and comprehensive, and it is easy to be confused by it.
Reference is a very important mechanism in JavaScript programming, but strangely, most developers will not pay attention to it or even understand it. Reference refers to the abstract relationship "code access to objects". It is somewhat similar to C/C++ pointers, but not the same thing. Reference is also the most critical mechanism of the JavaScript engine in garbage collection.
The following code is an example:
The code copy is as follows:
// ......
var val = 'hello world';
function foo() {
return function() {
return val;
};
}
global.bar = foo();
// ......
After reading this code, can you tell which objects still survive after this part of the code are executed?
According to relevant principles, the objects that are not recycled and released in this code include val and bar(). What exactly makes them unable to be recycled?
How does the JavaScript engine perform garbage collection? The garbage collection algorithm mentioned above is only used during recycling. So how does it know which objects can be recycled and which objects need to continue to survive? The answer is a reference to a JavaScript object.
In JavaScript code, even if you simply write down a variable name as a single line without doing anything, the JavaScript engine will think that this is an access behavior to the object and there is a reference to the object. In order to ensure that the garbage collection behavior does not affect the operation of the program logic, the JavaScript engine must not recycle the objects in use, otherwise it will be messy. Therefore, the standard for judging whether an object is in use is whether there is still a reference to the object. But in fact, this is a compromise, because JavaScript references can be transferred, so there is a possibility that some references are brought to the global scope, but in fact, they are no longer needed to access them in the business logic and should be recycled, but the JavaScript engine will still rigidly believe that the program still needs it.
How to use variables and references in the correct posture is the key to optimizing JavaScript from the language level.
3. Optimize your JavaScript
Finally got to the point. Thank you very much for seeing this with patience. After so many introductions above, I believe you have a good understanding of JavaScript's memory management mechanism. Then the following skills will make you feel better.
3.1 Make good use of functions
If you have the habit of reading excellent JavaScript projects, you will find that when developing front-end JavaScript code, many big guys often use an anonymous function to wrap it on the outermost layer of the code.
The code copy is as follows:
(function() {
// Main business code
})();
Some are even more advanced:
The code copy is as follows:
;(function(win, doc, $, undefined) {
// Main business code
})(window, document, jQuery);
Even front-end modular loading solutions such as RequireJS, SeaJS, OzJS, etc. all adopt a similar form:
The code copy is as follows:
// RequireJS
define(['jquery'], function($) {
// Main business code
});
// SeaJS
define('module', ['dep', 'underscore'], function($, _) {
// Main business code
});
If you say that many codes of Node.js open source projects are not processed in this way, then you are wrong. Before actually running the code, Node.js will wrap each .js file into the following form:
The code copy is as follows:
(function(exports, require, module, __dirname, __filename) {
// Main business code
});
What are the benefits of doing this? We all know that at the beginning of the article, we said that JavaScript can have scoped functions, with statements and global scope. We also know that objects defined in the global scope may survive until the process exits. If it is a large object, it will be troublesome. For example, some people like to render templates in JavaScript:
The code copy is as follows:
<?php
$db = mysqli_connect(server, user, password, 'myapp');
$topics = mysqli_query($db, "SELECT * FROM topics;");
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Are you a funny guy invited by monkeys? </title>
</head>
<body>
<ul id="topics"></ul>
<script type="text/tmpl" id="topic-tmpl">
<li>
<h1><%=title%></h1>
<p><%=content%></p>
</li>
</script>
<script type="text/javascript">
var data = <?php echo json_encode($topics); ?>;
var topicTmpl = document.querySelector('#topic-tmpl').innerHTML;
var render = function(tmlp, view) {
var compiled = tmlp
.replace(//n/g, '//n')
.replace(/<%=([/s/S]+?)%>/g, function(match, code) {
return '" + escape(' + code + ') + "';
});
compiled = [
'var res = "";',
'with (view || {}) {',
'res = "' + compiled + '";',
'}',
'return res;'
].join('/n');
var fn = new Function('view', compiled);
return fn(view);
};
var topics = document.querySelector('#topics');
function init()
data.forEach(function(topic) {
topics.innerHTML += render(topicTmpl, topic);
});
}
init();
</script>
</body>
</html>
This kind of code can often be seen in the works of novices. What are the problems here? If the amount of data obtained from the database is very large, the data variable will be idle after the front-end completes the template rendering. However, because this variable is defined in the global scope, the JavaScript engine will not recycle and destroy it. This will continue to exist in the old generation heap memory until the page is closed.
But if we make some very simple modifications and wrap a layer of functions outside the logical code, the effect will be very different. After the UI rendering is completed, the code's reference to data is also cancelled. When the outermost function is executed, the JavaScript engine begins to check the objects in it, and data can be recycled.
3.2 Never define global variables
We just talked about that when a variable is defined in the global scope, the JavaScript engine will not recycle and destroy it by default. This will continue to exist in the old generation heap memory until the page is closed.
Then we have always followed a principle: never use global variables. Although global variables are indeed very easy to develop, the problems caused by global variables are far more serious than the convenience it brings.
Make variables less likely to be recycled;
1. Confusion is easily caused when multiple people collaborate;
2. It is easy to be interfered in the scope chain.
3. In conjunction with the above wrapping function, we can also handle "global variables" through wrapping functions.
3.3 Manually unreference variables
If a variable is not needed in business code, then the variable can be manually dereferenced to make it recycled.
The code copy is as follows:
var data = { /* some big data */ };
// blah blah blah
data = null;
3.4 Make good use of callbacks
In addition to using closures for internal variable access, we can also use the now very popular callback function for business processing.
The code copy is as follows:
function getData(callback) {
var data = 'some big data';
callback(null, data);
}
getData(function(err, data) {
console.log(data);
Callback functions are a technology of Continuation Passing Style (CPS). This style of programming transfers the business focus of the function from the return value to the callback function. And it has many benefits over closures:
1. If the passed parameters are the basic type (such as strings, numeric values), the formal parameters passed in the callback function will be copied values, and it will be easier to be recycled after the business code is used;
2. Through callbacks, in addition to completing synchronous requests, we can also use them in asynchronous programming, which is a very popular writing style now;
3. The callback function itself is usually a temporary anonymous function. Once the request function is executed, the reference to the callback function itself will be cancelled and it will be recycled.
3.5 Good closure management
When our business needs (such as circular event binding, private attributes, callbacks with arguments, etc.) must use closures, please be careful about the details.
Loop binding events can be said to be a compulsory course for getting started with JavaScript closures. Let's assume a scenario: there are six buttons, corresponding to six events. When the user clicks the button, the corresponding events are output at the specified place.
The code copy is as follows:
var btns = document.querySelectorAll('.btn'); // 6 elements
var output = document.querySelector('#output');
var events = [1, 2, 3, 4, 5, 6];
// Case 1
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function(evt) {
output.innerText += 'Clicked ' + events[i];
};
}
// Case 2
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = (function(index) {
return function(evt) {
output.innerText += 'Clicked ' + events[index];
};
})(i);
}
// Case 3
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = (function(event) {
return function(evt) {
output.innerText += 'Clicked ' + event;
};
})(events[i]);
}
The first solution here is obviously a typical loop binding event error. I won't explain it in detail here. You can refer to my answer to a netizen in detail; the difference between the second and third solutions lies in the parameters passed in the closure.
The parameters passed in the second scheme are the current loop subscript, while the latter is directly passed into the corresponding event object. In fact, the latter is more suitable for large amounts of data applications, because in JavaScript functional programming, the parameters passed in function calls are basic types objects, so the formal parameters obtained in the function body will be a copy value, so that this value is defined as a local variable within the scope of the function body. After the event binding is completed, the events variable can be manually dereferenced to reduce the memory usage in the outer scope. Moreover, when an element is deleted, the corresponding event listening function, event object, and closure function are also destroyed and recycled.
3.6 Memory is not a cache
The role of caching in business development is of great importance and can reduce the burden of space-time resources. But it should be noted that you should not use memory as cache easily. Memory is a thing of every inch of land for any program development. If it is not a very important resource, please do not put it directly in memory, or formulate an expiration mechanism to automatically destroy the expiration cache.
4. Check the memory usage of JavaScript
In daily development, we can also use some tools to analyze and troubleshoot memory usage in JavaScript.
4.1 Blink/Webkit Browser
In the Blink/Webkit browser (Chrome, Safari, Opera etc.), we can use the Profiles tool of Developer Tools to perform memory checks on our programs.
4.2 Memory checking in Node.js
In Node.js, we can use node-heapdump and node-memwatch modules for memory checking.
The code copy is as follows:
var heapdump = require('heapdump');
var fs = require('fs');
var path = require('path');
fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);
// ...
The code copy is as follows:<span style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif; font-size: 14px; line-height: 1.5em;">After introducing node-heapdump into the business code, we need to send a SIGUSR2 signal to the Node.js process at a certain run time, and let node-heapdump take a snapshot of the heap memory. </span>
Copy the code as follows: $ kill -USR2 (cat app.pid)
In this way, there will be a snapshot file named in the format heapdump-<sec>.<usec>.heapsnapshot in the file directory. We can open it using the Profiles tool in the browser's Developer Tools and check it.
5. Summary
The article is coming soon again. This sharing mainly shows you the following content:
1. JavaScript is closely related to memory usage at the language level;
2. Memory management and recycling mechanisms in JavaScript;
3. How to use memory more efficiently so that the produced JavaScript can be more expanded and energetic;
4. How to perform memory checks when encountering memory problems.
I hope that through learning this article, you can produce better JavaScript code to make your mother feel at ease and your boss feel at ease.