{"componentChunkName":"component---src-templates-blog-post-js","path":"/garbage-collection-mechanism-of-v8-engine/","result":{"data":{"markdownRemark":{"html":"<p>Js不会让程序员自己开辟或释放内存，而是有自己的一套垃圾回收算法，来进行自动的内存管理</p>\n<h2>V8内存限制</h2>\n<p>V8只能使用系统的一部分内存，具体来说，在64位系统下，V8最多只能分配1.4G, 在32位系统中，最多只能分配0.7G。你想想在前端这样的大内存需求其实并不大，但对于后端而言，nodejs如果遇到一个2G多的文件，那么将无法全部将其读入内存进行各种操作了。<br>\n所有的对象类型的数据在JS中都是通过堆进行空间分配的。当我们构造一个对象进行赋值操作的时候，其实相应的内存已经分配到了堆上。你可以不断的这样创建对象，让V8为它分配空间，直到堆的大小达到上限。设置内存上限是因为两个方面：<br>\n第一，JS是单线程运行的，这意味着一旦进入到垃圾回收，那么其它的各种运行逻辑都要暂停; 另一方面垃圾回收其实是非常耗时间的操作</p>\n<blockquote>\n<p>以 1.5GB 的垃圾回收堆内存为例，V8 做一次小的垃圾回收需要50ms 以上，做一次非增量式(ps:后面会解释)的垃圾回收甚至要 1s 以上。</p>\n</blockquote>\n<p>可见其耗时之久，而且在这么长的时间内，我们的JS代码执行会一直没有响应，造成应用卡顿，导致应用性能和响应能力直线下降。因此，V8 做了一个简单粗暴的选择，那就是限制堆内存，也算是一种权衡的手段，因为大部分情况是不会遇到操作几个G内存这样的场景的。</p>\n<h2>调整内存限制</h2>\n<div class=\"gatsby-highlight\" data-language=\"shell\"><pre class=\"language-shell\"><code class=\"language-shell\">// 这是调整老生代这部分的内存，单位是MB。后面会详细介绍新生代和老生代内存\nnode --max-old-space-size<span class=\"token operator\">=</span><span class=\"token number\">2048</span> xxx.js</code></pre></div>\n<p>或者</p>\n<div class=\"gatsby-highlight\" data-language=\"shell\"><pre class=\"language-shell\"><code class=\"language-shell\">// 这是调整新生代这部分的内存，单位是 KB。\nnode --max-new-space-size<span class=\"token operator\">=</span><span class=\"token number\">2048</span> xxx.js</code></pre></div>\n<h2>新生代内存的回收</h2>\n<p>V8把堆内存分成了两部分进行处理——新生代内存和老生代内存。顾名思义，新生代就是临时分配的内存，存活时间短， 老生代是常驻内存，存活的时间长。V8 的堆内存，也就是两个内存之和。<br>\n新生代的垃圾回收是怎么做的呢，首先，新生代内存空间一分为二:<br>\n其中From部分表示正在使用的内存，To 是目前闲置的内存。</p>\n<p>当进行垃圾回收时，V8将From部分的对象检查一遍，如果是存活对象那么复制到To内存中(在To内存中按照顺序从头放置的)，如果是非存活对象直接回收即可。</p>\n<p>当所有的From中的存活对象按照顺序进入到To内存之后，From 和 To 两者的角色对调，From现在被闲置，To为正在使用，如此循环。</p>\n<h2>老生代内存的回收</h2>\n<p>刚刚学习了新生代的回收方式，那么新生代中的变量如果经过多次回收后依然存在，那么就会被放入到老生代内存中，这种现象就叫晋升。</p>\n<p>发生晋升其实不只是这一种原因，我们来梳理一下会有那些情况触发晋升:</p>\n<ul>\n<li>已经经历过一次 Scavenge 回收。</li>\n<li>To（闲置）空间的内存占用超过25%。</li>\n</ul>\n<p>现在进入到老生代的垃圾回收机制当中，老生代中累积的变量空间一般都是很大的，当然不能用Scavenge算法啦，浪费一半空间不说，对庞大的内存空间进行复制岂不是劳民伤财？</p>\n<p>那么对于老生代而言，究竟是采取怎样的策略进行垃圾回收的呢？</p>\n<p>第一步，进行标记-清除。这个过程在《JavaScript高级程序设计(第三版)》中有过详细的介绍，主要分成两个阶段，即标记阶段和清除阶段。首先会遍历堆中的所有对象，对它们做上标记，然后对于代码环境中使用的变量以及被强引用的变量取消标记，剩下的就是要删除的变量了，在随后的清除阶段对其进行空间的回收。</p>\n<p>当然这又会引发内存碎片的问题，存活对象的空间不连续对后续的空间分配造成障碍。老生代又是如何处理这个问题的呢？</p>\n<p>第二步，整理内存碎片。V8 的解决方式非常简单粗暴，在清除阶段结束后，把存活的对象全部往一端靠拢。</p>\n<h2>增量标记</h2>\n<p>由于JS的单线程机制，V8 在进行垃圾回收的时候，不可避免地会阻塞业务逻辑的执行，倘若老生代的垃圾回收任务很重，那么耗时会非常可怕，严重影响应用的性能。那这个时候为了避免这样问题，V8 采取了增量标记的方案，即将一口气完成的标记任务分为很多小的部分完成，每做完一个小的部分就\"歇\"一下，就js应用逻辑执行一会儿，然后再执行下面的部分，如果循环，直到标记阶段完成才进入内存碎片的整理上面来。其实这个过程跟React Fiber的思路有点像，这里就不展开了。</p>\n<p>经过增量标记之后，垃圾回收过程对JS应用的阻塞时间减少到原来了1 / 6, 可以看到，这是一个非常成功的改进。</p>","timeToRead":4,"frontmatter":{"title":"v8引擎垃圾内存回收机制","date":"August 11, 2020","spoiler":null},"fields":{"slug":"/garbage-collection-mechanism-of-v8-engine/"}}},"pageContext":{"slug":"/garbage-collection-mechanism-of-v8-engine/","previous":{"fields":{"slug":"/javascript-modularization-summary/"},"frontmatter":{"title":"JavaScript模块化总结"}},"next":{"fields":{"slug":"/classic-topic-what-happened-from-URL-input-to-page-rendering/"},"frontmatter":{"title":"经典题目：从输入URL到页面呈现发生了什么"}}}},"staticQueryHashes":["3649515864","63159454"]}