📜 ⬆️ ⬇️

How JS works: memory management, four types of memory leaks and how to deal with them

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


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:

  1. All variable values ​​and other data used by programs.
  2. 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 memory

When 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(); //  ,   ... //    n  

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 allocation
Static 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; // o1   o2 o2.p = o1; // o2   o1.   . } f(); 


Circular link

Algorithm "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:



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 collection

Although 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. , . :

  1. , .
  2. , ( ) . , - null , , .
  3. .

. , , . , , , .

?


, , , - .


. , , , , . , , .

. . .

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"; } //     ,   this     (window), // this   undefined, ,    ,      foo(); 

, , "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); //     5 . 

, , 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); //  - element.removeEventListener('click', onClick); element.parentNode.removeChild(element); // ,       , // ,      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() {   //      body.   document.body.removeChild(document.getElementById('image'));   //         #button    //   elements.  ,  button   //     ,       . } 

, DOM .

, ( <td>) JS-. DOM, . , , , , , . - . . , , . , DOM .

Results


, , , . , , , , . JS- , , .

Dear readers! JavaScript-? — , , .

Source: https://habr.com/ru/post/338150/


All Articles