[We advise you to read] Other 19 parts of the cycle In the third article from the series, which focuses on the features of JavaScript, we will talk about memory. This topic is extremely important, however, developers often ignore it. This situation is based on various reasons, among which are the ever-increasing complexity of modern programming languages ​​and the progress in the development of automatic memory management tools. In addition to the story about the JS memory model, we will share with you some tips to combat memory leaks.

According to the author of the article,
SessionStack uses methods to prevent memory leaks in order to prevent unnecessarily high memory consumption in web applications that integrate their development.
Overview
Some languages, such as C, have low-level memory management tools, such as
malloc()
and
free()
. These basic functions are used by developers when interacting with the operating system to explicitly allocate and free memory.
')
At the same time, JavaScript allocates memory when something (objects, strings, and so on) is created, and “automatically”, when it is no longer in use, frees it up during a process called
garbage collection . This seemingly “automatic” nature of resource release is a source of confusion and gives developers using JavaScript (and other high-level languages) a false sense that they may not care at all about memory management.
This is a big mistake .
Even programming in high-level language, developers need to understand the principles, or at least know the basics of memory management. Sometimes problems arise in the automatic memory management system (such as errors, restrictions in the implementation of the garbage collector, and so on), the nature of which developers must understand in order to correct them (or at least find the right ways to circumvent them, requiring minimal additional effort and not too large amounts of auxiliary code).
Memory life cycle
Regardless of the programming language, the memory life cycle almost always looks the same:
Life cycle of memory: allocation, use, release- Memory allocation - memory is allocated by the operating system, which allows the program to use the resources provided for it. In low-level languages ​​(such as C), this is an explicit operation that the developer needs to perform. In high-level languages, however, this problem is solved automatically.
- Memory usage is the time when the program performs any operations with previously allocated memory. At this stage, when accessing variables, read and write operations are performed.
- Memory freeing - at this stage of the memory life cycle, the memory is released, which the program no longer needs, that is, it is returned to the system. As in the case of memory allocation , the release is an explicit operation in low-level languages.
Recall that in the
first article of this cycle, you can read about the call stack and the heap.
What is memory?
Before we consider the issues of working with memory in JavaScript, let's talk, in a nutshell, about what memory is.
At the hardware level, computer memory consists of many
triggers . Each trigger consists of several transistors, it is able to store one bit of data. Each of the triggers has a unique address, so their contents can be read and rewritten. Thus, conceptually, we can perceive computer memory as a huge array of bits that can be read and written.
However, programmers are people, not computers; it is not particularly convenient for them to operate with individual bits. Therefore, bits are usually organized into larger structures that can be represented as numbers. 8 bits form 1 byte. In addition to the bytes, there is such a thing as words (sometimes - 16 bits long, sometimes 32).
Many things are stored in memory:
- All variable values ​​and other data used by programs.
- Program code, including the operating system code.
The compiler and the operating system jointly perform the bulk of the memory management work without the direct participation of the programmer, however, we encourage developers to ask about what is happening in the depths of the memory management mechanisms.
When the code is compiled, the compiler can examine primitive data types and calculate in advance the amount of memory needed to work with them. The required amount of memory is then allocated to the program in the call stack space. The space in which space is allocated for variables is called the stack space, since when functions are called, the memory allocated to them is located at the top of the stack. When returning from functions, they are removed from the stack in the order of LIFO (the last to come is the first to go, Last In First Out). For example, consider the following variable declarations:
int n; // 4 int x[4]; // 4- 4 double m; // 8
The compiler, after reviewing this code fragment (abstracting here from everything except the size of the data itself), can immediately find out that it will take 4 + 4 Ă— 4 + 8 = 28 bytes to store the variables.
It should be noted that the given sizes of integer variables and numbers with double precision reflect the current state of affairs. Approximately 20 years ago, integers were usually represented as 2-byte constructs, 4 bytes were used for double precision numbers. The code should not depend on the byte sizes of the basic data types.
The compiler will generate code that will interact with the operating system, requesting the required number of bytes in the stack to store variables.
In the above example, the compiler knows the addresses of the memory areas where each variable is stored. In fact, if we use the name of the variable
n
in the code, it is converted to an internal representation that looks like this: “memory address 4127963”.
Note that if we try to access the array element from our example using the
x[4]
construct
x[4]
, we will in fact refer to the data that corresponds to the variable
m
. This is due to the fact that the array element with index 4 does not exist; a record of the form
x[4]
will indicate a memory area that is 4 bytes farther than that memory area allocated for the last of the array elements -
x[3]
. An attempt to access
x[4]
may end up reading (or rewriting) some bits of the variable
m
. This, almost guaranteed, will lead to undesirable consequences in the course of program execution.
Location of variables in memoryWhen a function calls another function, each of them gets its own stack. All their local variables and a command pointer that stores data about where the call was made are stored here. When the function completes, the memory blocks occupied by it are made available again for other purposes.
Dynamic memory allocation
Unfortunately, everything gets complicated when, at compile time, we do not know how much memory we need for a variable. Imagine that we want to do something like the following:
int n = readInput();
In this situation, the compiler does not know how much memory is needed to store the array, since the size of the array determines the value that the user enters.
As a result, the compiler will not be able to reserve memory for a variable on the stack. Instead, our program will have to explicitly ask the operating system for the right amount of memory during its execution. This memory is allocated in the so-called
heap . The following table lists the main differences between static and dynamic memory allocation.
The difference between static and dynamic memory allocationStatic memory allocation
| Dynamic memory allocation
|
Volume must be known at compile time.
| Volume may not be known at compile time.
|
Produced at compile time.
| Produced during program execution.
|
Memory is allocated on the stack.
| Memory is allocated on the heap.
|
FILO memory allocation order (first entered - last left, First In Last Out)
| There is no defined order of memory allocation.
|
In order to fully understand how dynamic memory allocation works, we will need to discuss the concept of
pointers in detail, but this way we will deviate too much from our main topic. If you are interested in this, let the
author of this material know, and perhaps this topic will be raised in future publications.
JavaScript memory allocation
Now we will talk about how the first step of the life cycle of memory (allocation) is implemented in JavaScript.
JavaScript frees a developer from responsibility for managing memory allocation. JS does it on its own, along with the declaration of variables.
var n = 374; // var s = 'sessionstack'; // var o = { a: 1, b: null }; // var a = [1, null, 'str']; // // ( ) function f(a) { return a + 3; } // ( ) // someElement.addEventListener('click', function() { someElement.style.backgroundColor = 'blue'; }, false);
Calls to some functions also lead to the allocation of memory for an object:
var d = new Date(); // Date var e = document.createElement('div'); // DOM
Method calls can also lead to memory allocation for new values ​​or objects:
var s1 = 'sessionstack'; var s2 = s1.substr(0, 3); // s2 - // , // JavaScript , // [0, 3]. var a1 = ['str1', 'str2']; var a2 = ['str3', 'str4']; var a3 = a1.concat(a2); // 4 // a1 a2
Javascript memory usage
Using allocated memory in JavaScript usually means reading and writing it.
This can be done by reading or writing the value of a variable or an object property, or even by passing in a function argument.
Freeing up memory that is no longer needed.
Most memory management problems occur at this stage.
The biggest challenge is figuring out when the allocated memory is no longer needed by the program. This often requires the developer to determine where in the program a certain fragment of memory is no longer needed and would release it.
High-level languages ​​have a built-in subsystem called the
garbage collector . The role of this subsystem is to track memory allocation operations and use it to find out when a fragment of allocated memory is no longer needed. If so, the garbage collector can automatically free this fragment.
Unfortunately, this process does not differ in absolute accuracy, since the general problem of finding out whether a fragment of memory is needed or not is
insoluble (it cannot be solved by algorithmic solutions).
Most garbage collectors work by collecting memory that cannot be accessed, that is, such all the variables that point to are inaccessible. This, however, is too bold an assumption about the possibility of freeing memory, since at any time a certain area of ​​memory may have variables pointing to it in a certain area of ​​visibility, although it will never work in this program anymore.
Garbage collection
The basic concept that garbage collection algorithms rely on is the concept of
links .
In the context of memory management, an object refers to another object if the first, explicitly or implicitly, has access to the latter. For example, a JavaScript object has a link to its own
prototype (
explicit link ) and to the values ​​of the properties of the prototype (
implicit link ).
Here, the idea of ​​an “object” expands to something larger than the usual JS object, which also includes functional scopes (or the global lexical scope).
The principle of lexical scope allows you to set the rules for resolving variable names in nested functions. Namely, the nested functions contain the scope of the parent function even if the return is from the parent function.
Garbage collection based on reference counting
This is the simplest garbage collection algorithm. The object is considered fit for destruction if it is not indicated by
any references.
Take a look at the following code:
var o1 = { o2: { x: 1 } }; // 2 . // o2 o1, . // . var o3 = o1; // o3 - , // , o1. o1 = 1; // , o1, // , o3 var o4 = o3.o2; // o2 . // 2 . - // . // - o4 o3 = '374'; // , o1, // . // . // , o2 // o4. , , // . o4 = null; // o2 , o1, // , // .
Circular links - source of problems
When cyclical links come into play, some limitations of the above-described search model for unnecessary objects appear. The following example creates two objects that link to each other. A circular reference is formed. Objects will be out of reach after a function call, that is, they are useless, and the memory they occupy could be freed. However, the reference counting algorithm considers that since each of the two objects has at least one reference, none of them can be destroyed.
function f() { var o1 = {}; var o2 = {}; o1.p = o2;
Circular linkAlgorithm "mark and throw away"
In order to decide whether to save an object, the “mark and sweep” algorithm determines the object's reachability.
The algorithm consists of the following steps:
- The garbage collector builds a list of "root objects." Such objects are usually global variables referenced in the code. In JavaScript, an example of a global variable that can play the role of the root object is the
window
object.
- All root objects are scanned and marked as active (that is, it is not “garbage”). Also, recursively, all child objects are viewed. Everything that can be accessed from the root objects is not considered "garbage".
- All areas of memory that are not marked as active can be considered suitable for processing by the garbage collector, which can now free this memory and return it to the operating system.
Visualization algorithm "mark and throw away"This algorithm is better than the previous one, since the situation “to the object is not referenced” leads to the fact that the object is unattainable. The converse assertion, as demonstrated in the section on cyclical links, is not true.
Since 2012, all modern browsers have been equipped with garbage collectors, based on the “mark and throw away” algorithm. In recent years, all the improvements made in the field of garbage collection in JavaScript (this is genealogical, incremental, competitive, parallel garbage collection) are improvements to this algorithm, without changing its basic principles, which consist in determining the reachability of an object.
In
this article you can find details about the garbage collection model considered here.
Solving the problem of circular references
In the first of the examples above, after returning from a called function, two objects no longer refer to something that can be addressed from the scope of the global object. Therefore, the garbage collector will find them unreachable.
Circular links do not interfere with garbage collectionAlthough objects refer to each other, they cannot be accessed from the root object.
Paradoxical behavior of garbage collectors
Although garbage collectors are convenient, you have to make certain trade-offs when using them. One of them is non-determinism. In other words, garbage collectors are unpredictable. It is impossible to say exactly when the garbage collection will be performed. This means that in some cases programs use more memory than they actually need.
In other cases, short pauses caused by garbage collection may be noticeable in performance-demanding applications. Although unpredictability means that it is not possible to know exactly when garbage collection will be performed, most garbage collectors use the same pattern for performing memory freeing operations. Namely, they do this when allocating memory. , . :
- , .
- , ( ) . , -
null
, , .
- .
. , , . , , , .
?
, , , - .
. , , , ,
. , , .
. .
.
JavaScript.
JavaScript
â–Ť
JavaScript . . ,
window
. :
function foo(arg) { bar = "some text"; }
:
function foo(arg) { window.bar = "some text"; }
bar
foo
,
var
, .
, , , .
, ,
this
:
function foo() { this.var1 = "potential accidental global"; }
, ,
"use strict";
JS-. , .
.
, , . , , (
null
- ). , , . ,
null
- , , , .
â–Ť
JS-
setInterval —
.
, , , , , , , . ,
setInterval
:
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000);
, , DOM , .
,
renderer
, , , , . , , , . , , . , . , ,
serverData
, , , , .
, , ( , ).
, ( IE6, ) . , , . , . For example:
var element = document.getElementById('launch-button'); var counter = 0; function onClick(event) { counter++; element.innerHtml = 'text ' + counter; } element.addEventListener('click', onClick);
( Internet Explorer Microsoft Edge) , . ,
removeEventListener
, .
, , jQuery, ( API). , , , , IE 6.
â–Ť
JavaScript — . — , , . JavaScript :
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // originalThing console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
,
replaceThing
,
theThing
, (
someMethod
). ,
unused
,
originalThing
( ,
theThing
replaceThing
). , ? ,
, , .
,
someMethod
,
unused
.
originalThing
. ,
unused
,
someMethod
theThing
replaceThing
( — ). ,
someMethod
unused
,
originalThing
,
unused
, , ( — ). .
, . . , (
theThing
), , .
Meteor,
, .
â–Ť DOM DOM
DOM . , , . . DOM: DOM, — . , , .
var elements = { button: document.getElementById('button'), image: document.getElementById('image') }; function doStuff() { image.src = 'http://example.com/image_name.png'; } function removeImage() {
, DOM .
, ( <td>) JS-. DOM, . , , , , , . - . . , , . , DOM .
Results
, , , . , , , , . JS- , , .
Dear readers! JavaScript-? — , , .