preface

In fact, most of the time as a javascript developer you don’t need to worry too much about memory usage and release, because all javascript environments implement garbage Collector (GC)), but as spAs become more and more large, the pursuit of extreme performance is increasingly demanding that developers have a proper understanding of the internal implementation of the garbage collection mechanism, which can help in performance optimization and memory leak tracking. Look at a memory leak

var theThing = null; var replaceThing = function () { var originalThing = theThing; var c = 'a' function unused() { if (originalThing) { console.log("hi"); }}; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log('1111'); }}; }; setInterval(replaceThing,1000)Copy the code

One of the first things I wanted to know more about javacript GC was that I was looking for a memory leak. Any kind of GC management needs to do these steps:

  1. Identify which objects need to be reclaimed.
  2. Reclaim/reuse the memory of objects that need to be reclaimed.
  3. Compress/defragment memory (some may not)

Common mechanisms to identify whether objects need to be reclaimed are as follows:

  • Reference Counting (Python)
  • Escape analysis (Java)
  • Tracing/Reachable Tracing

Today I’ll focus on the implementation of GC in V8

Tracing/Reachable Tracing analysis

The first step in GC is to figure out which objects need to be reclaimed and which do not. In Tracing/Reachable, objects that are considered reachability are considered unrecyclable, and the remaining untraceable objects are the objects to be reclaimed. In V8, trace analysis starts with the Root object (GC Root), which in javascript includes the call stack and the global object, and marks all traceable objects as reachable based on Pointers.

The Generational Hypothesis

Generational Hypothesis means that most subjects need to be recycled early on. Based on this assumption, many programming languages have garbage collection mechanisms designed to divide memory into generations, young generation and old generation. The generation here is essentially creating two Spaces for objects that have just been allocated and objects that have gone through two GC cycles but have not been collected. In V8, there are two garbage collectors, one for the young generation and the other for the old generation. Scavenger collects garbage for the young generation and The Major GC collects garbage for the old generation. Their algorithms are also different.

Scavenger

V8 uses the semi-space algorithm for the younger generation of memory space, which means that memory is divided into two parts, with only one part of memory available and the other half completely empty (or allocated). At the start of program execution, all variables are allocated into the available half of memory (called from-space). When the first GC starts, all the reachable objects can be transferred to the remaining half of the to-space based on the traceable analysis results. Thus, the from-space memory can be allocated again. At this time, if there are any newly declared objects that need to be allocated, the memory will be allocated to this block. Finally, after the transfer of the objects that can not be freed, the reference pointer will be updated to point to the latest address in the to-space.

At the start of the second GC, objects that are still unfreed in the original to-space are first moved to the Spaces of the old generation, at which point all of the to-space can be allocated again and the previous operation is repeated. To move an object from from-space that cannot be freed. After two GC’s, objects that survived two GC’s are now in the old generation, and objects that survived one GC’s are now in the to-space, also known as intermediate Generation. There are three processes to reclaim memory in Scavenger: marking (trace analysis), shifting (frost-space to to-space), and updating pointer addresses.

One of the problems with this reclamation mechanism is that moving objects costs some performance, but according to the Generational Hypothesis, most of the objects are recycled early on, which means that only a small number of uncollectable objects need to be moved, This also means that if this assumption is not true, for example, there are too many closures in our code so that too many scopes cannot be released, then there will be too many objects that need to be moved between Spaces, which is a waste of performance. On the contrary, if most objects can be reclaimed early, this mechanism does nothing for the objects that need to be reclaimed early, except to move the few objects that can’t be freed (rewrite dead object), and then rewrite dead object in the next memory allocation.

Parallel

Scheduling threads in Parallel is V8 memory recovery of an algorithm, refers to the main thread and help thread in memory of the same work at the same time, the algorithm will stop all the work of the main thread going on, but the effort is split into several threads, theory of time also be divided exactly by participating in the number of threads (plus some synchronized scheduling Pin). Scavenger uses this thread scheduling mechanism. When it comes time to reclaim, all threads acquire a certain number of references to live objects and start moving them into to-space at the same time. Different threads may access the same object through different reference paths. When the thread moves the living object to the to-space, after updating the pointer address, it will leave a forwarding pointer in the old object of the from-space, so that other threads can use this pointer to find the new address after finding the object New reference address.

Major GC

The Major GC is mainly responsible for the collection of old generation memory, which is also three processes: marking (trace analysis), cleaning, and defragmenting compressed memory. The tagging step, like Scavenger, uses a trace analysis to determine which memory needs to be reclaimed. After the object is reclaimed, the reclaimed memory is added to the free-list data structure, which is like a drawer. The size of each drawer represents the amount of memory that can be allocated consecutively from this address , when we need to reallocate memory in the old generation, we can quickly find a suitable drawer to allocate memory according to the size of the required memory. The last step is to defragment the memory, which is similar to defragmenting a disk in Windows, by copying objects that have not survived to other defragmenting pages using free-list look-ups, so that small pieces of memory can be defragmented and used later. As in Scavenger, there is a performance cost to copying objects back and forth. In V8, only highly fragmented pages are collated and other pages are cleared so that only surviving objects can be moved.

Concurrent

Concurrent is also V8’s thread scheduling algorithm for memory reclamation, helping threads synchronize some of the work of memory reclamation while the main thread executes Javascript. This algorithm is much more complex than Parallel, where one millisecond helps the thread GC and the next millisecond the main thread changes the object. It is also possible to help threads and mainthreads read and modify the same object at the same time. But the advantage of this algorithm is that while the helper thread is doing GC work, the main thread can continue to execute JavaScript without being affected. The Major GC uses this algorithm and starts Major when the memory of the older generation reaches a threshold that the system automatically calculates When the reachable object is referenced by the reference pointer, the main thread is still executing JavaScript. When the reachable object is referenced by the reference pointer, the main thread is still executing JavaScript. Tag when help thread is complete, or the old generation touched the setting threshold, the main thread also started to participate in GC, his first step fast markup validation, to ensure that help the thread in the object is modified while the main thread marking correctly (in helping thread marked, if the main thread of modified the object will be Write JavaScript Barriers, like having a mark. After the main thread confirms that all living objects have been marked, it works with several child threads to compress and update Pointers to some of the memory pages. Not all pages need to be compressed (only the highly fragmented ones), and free-list cleaning is done without compression.

When will GC be performed

There is no way to programmatically trigger a GC actively in JavaScript because of the complexity of thread scheduling involved, and an active GC can affect either the ongoing GC or the next GC. For Scavenger, when allocating memory in the new generation, when there is no space to allocate memory, the Scavenger garbage collection starts, hoping to free some memory, and then tries to reallocate memory. For the older generation, the time to start garbage collection is much more complicated. In simple terms, an appropriate threshold is calculated based on the percentage of memory used by the older generation and the percentage of objects that need to be allocated. When this threshold is reached, garbage collection of the older generation will be started.

We can manually set the space size of the new generation and the old generation:

    node --max-old-space-size=1700 index.js
    node --max-new-space-size=1024 index.js
Copy the code

The GC to do in your spare time

Although there is no way to actively trigger GC through JavaScript, there is a mechanism for idle GC in V8 that determines when it is idle based on the host being embedded. V8 in Chrome, for example, in order to ensure smooth animation rendering, going to render 60 frames a second, the equivalent of 16.6 milliseconds to render a frame, finished within 16.6 milliseconds to render a frame, such as only spent 10 milliseconds finished rendering the a frame of the animation, then you have free time of about 6.6 millisecond can perform GC (some free time In many newer browsers, developers can also use the requestIdleCallback event to take advantage of browser idle time to improve performance. For those interested, check out the React 16 Fiber implementation.)

Incremental

Can you do a GC in a few milliseconds of free time? Incremental is another scheduling algorithm. Compared to other scheduling algorithms, where the main thread is paused once to execute a complete GC,Incremental requires that the entire GC be broken up into small pieces, and that JS is executed progressively with the main thread, or when the main thread has idle time Small block GC tasks.

conclusion

Different JavaScript engines implement GC to varying degrees, and this article focuses on V8 as an example. There are many areas that are not fully explored, such as: In fact, there is not only one space in the old generation, but four Spaces, each storing different data (old space,large Object space,matedata space,code space). Garbage collection design itself is a very complex program, with GC, developers can completely not worry about memory management issues. But a proper understanding of how garbage collection works can help us better understand the JavaScript environment and write more efficient code.

Finally, the previous memory leak code is deduced step by step:

  1. Two variables theThing and replaceThing are first declared in global scope, where replaceThing is assigned a method (callable) Object), and then call the setInterval method, calling replaceThing every 1000 milliseconds.
  2. Hoist: hoist: unused: declared originThing and c. Hoist: unused: declared originThing and c. It is important to note that a closure is created at the time the method is declared, not when it is executed, so a closure containing originThing, the local scoped variable used by the unused method, is also created. Also in V8 once scope have closures, the context will be bound to as closure of all the methods, even if this method doesn’t use any the scope of a variable, so here give global scope assignment, someMethod as a method, also bind an unused create closure, in the global and is assigned a value Use theThing in the domain.
  3. If the first GC starts at this time, do Reachable analysis from the global object :theThing(Reachable),replaceThing(Reachable),theThing->longStr(Reachable),theThing->someM Ethod (reachable),execution stack -> setInterval -> Closure -> originThing(reachable). All marks complete. At this time:
          from-space                                to-space

    theThing         (reachable)                theThing
    replaceThing     (reachable)                replaceThing
    unused                                      originThing
    originThing      (reachable)       =>       longStr  
    c                                           someMethod
    longStr          (reachable)                
    someMethod       (reachable)                
Copy the code
  1. After 1000 ms, replaceThing is executed again and step 2 is repeated
  2. The second GC starts
          from-space                                to-space                           old-space

    theThing         (reachable)                theThing                             originThing -> theThing
    replaceThing     (reachable)                replaceThing                         theThing -> longStr
    unused                                      originThing                          theThing -> someMethod
    originThing      (reachable)       =>       longStr= >        someMethod -> originThing(closure)        
    c                                           someMethod
    longStr          (reachable)                
    someMethod       (reachable)                
Copy the code
  1. Because the closure was always attached to the originThing, the originThing in the old space could not be released. Over time, the replaceThing method is executed every 1000 milliseconds
         old-space
    originThing -> theThing -> longStr & someMethod -> originThing(closure)
    originThing -> theThing -> longStr & someMethod -> originThing(closure)
    originThing -> theThing -> longStr & someMethod -> originThing(closure)
    originThing -> theThing -> longStr & someMethod -> originThing(closure)
    originThing -> theThing -> longStr & someMethod -> originThing(closure)
Copy the code

conclusion

The main cause of memory leaks is

Then,the someMethod in originalThing references the old Thing, so that nothing reachable can be released.

var theThing = null; var replaceThing = function () { var originalThing = theThing; var c = 'a' function unused() { if (originalThing) { console.log("hi"); }}; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log('1111'); }}; originalThing = null; // Manually release local scope variables}; setInterval(replaceThing,1000)Copy the code