>웹 프론트엔드 >JS 튜토리얼 >일반적인 JavaScript 메모리 누수

일반적인 JavaScript 메모리 누수

小云云
小云云원래의
2017-12-05 16:31:451745검색

메모리 누수란 무엇인가요

메모리 누수는 프로그램의 부주의나 오류로 인해 더 이상 사용되지 않는 메모리를 해제하지 못하는 것을 말합니다. 메모리 누수는 메모리가 물리적으로 사라지는 것을 의미하는 것이 아니라, 애플리케이션이 특정 메모리 세그먼트를 할당한 후 설계 오류로 인해 해당 메모리 세그먼트가 해제되기 전에 해당 메모리 세그먼트에 대한 제어권을 상실하여 오류가 발생하는 것을 의미합니다. 기억력 낭비.
메모리 누수는 일반적으로 프로그램 소스 코드에 액세스할 수 있는 프로그래머만 분석할 수 있습니다. 그러나 엄밀히 말하면 정확하지는 않더라도 메모리 사용량이 원치 않게 증가하는 것을 메모리 누수로 설명하는 데 익숙한 사람이 꽤 있습니다.
————wikipedia

예기치 않은 전역 변수

JavaScript가 선언되지 않은 변수를 처리하는 방법: 전역 개체의 변수에 대한 참조를 만듭니다(즉, 변수가 아닌 전역 개체의 속성입니다. 삭제를 통해 삭제할 수 있습니다.) 브라우저에 있는 경우 전역 개체는 창 개체입니다.

선언되지 않은 변수가 많은 양의 데이터를 캐시하는 경우 창을 닫거나 페이지를 새로 고칠 때만 데이터를 해제할 수 있습니다. 이로 인해 예기치 않은 메모리 누수가 발생할 수 있습니다.

<span style="font-size: 14px;">function foo(arg) {<br>    bar = "this is a hidden global variable with a large of data";<br>}<br></span>

는 다음과 같습니다.

<span style="font-size: 14px;">function foo(arg) {<br>    window.bar = "this is an explicit global variable with a large of data";<br>}<br></span>

또한 다음을 통해 예상치 못한 전역 변수를 만듭니다.

<span style="font-size: 14px;">function foo() {<br>    this.variable = "potential accidental global";<br>}<br><br>// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'<br>foo();<br></span>

해결책:

엄격 모드를 켜려면 JavaScript 파일에 'use strict'를 추가하세요. , 위의 문제를 효과적으로 피할 수 있습니다.

<span style="font-size: 14px;">function foo(arg) {<br>    "use strict" // 在foo函数作用域内开启严格模式<br>    bar = "this is an explicit global variable with a large of data";// 报错:因为bar还没有被声明<br>}<br></span>

함수에서 전역 변수를 사용해야 하는 경우 다음 코드와 같이 창에서 이를 명시적으로 선언할 수 있습니다.

<span style="font-size: 14px;">function foo(arg) {<br>    window.bar = "this is a explicit global variable with a large of data";<br>}<br></span>

가독성이 높을 뿐만 아니라 향후 유지 관리에도 편리합니다

전역 변수라고 하면 대용량 데이터를 임시로 저장하는 데 사용되는 전역 변수에 주의가 필요합니다. 반드시 null로 설정하거나 데이터 처리 후 재할당해야 합니다. 일반적으로 캐시는 성능 최적화를 위해 사용되며, 캐시 크기에 상한선을 설정하는 것이 가장 좋습니다. 캐시는 회수할 수 없으므로 캐시가 높을수록 메모리 소비도 많아집니다.

console.log

console.log: 개발 중 디버깅 및 분석에 자주 사용되는 웹 개발 콘솔에 메시지를 인쇄합니다. 때로는 개발 중에 일부 개체 정보를 인쇄해야 하지만 게시할 때 console.log 문을 제거하는 것을 잊었습니다. 이로 인해 메모리 누수가 발생할 수 있습니다.

console.log에 전달된 개체는 가비지 수집될 수 없습니다. ♻️ 코드 실행 후 개체 정보를 개발 도구에서 확인해야 하기 때문입니다. 따라서 프로덕션 환경에서는 어떤 개체도 console.log에 기록하지 않는 것이 가장 좋습니다.

예------>demos/log.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Leaker</title><br></head><br><br><body><br>  <input type="button" value="click"><br>  <script><br>    !function () {<br>      function Leaker() {<br>        this.init();<br>      };<br>      Leaker.prototype = {<br>        init: function () {<br>          this.name = (Array(100000)).join('*');<br>          console.log("Leaking an object %o: %o", (new Date()), this);// this对象不能被回收<br>        },<br><br>        destroy: function () {<br>          // do something....<br>        }<br>      };<br>      document.querySelector('input').addEventListener('click', function () {<br>        new Leaker();<br>      }, false);<br>    }()<br>  </script><br></body><br><br></html><br></span>

여기에 Chrome의 Devtools–>성능을 결합하여 몇 가지 분석을 수행합니다.

⚠️참고 : 브라우저 플러그인이 분석 결과에 영향을 미치지 않도록 숨겨진 창에서 분석 작업을 수행하는 것이 가장 좋습니다

  1. [성능] 항목의 기록을 활성화

  2. CG를 한 번 실행하고 기준선 참조선

  3. 버튼을 세 번 연속 클릭 [클릭]하면 세 개의 Leaker 개체가 생성됩니다

  4. CG 한 번 실행

  5. 녹화 중지

일반적인 JavaScript 메모리 누수

그것 [JS Heap] 라인이 최종 레벨로 떨어지지 않은 것을 볼 수 있습니다. 기준선 참조 라인 위치에는 분명히 회수되지 않은 메모리가 있습니다. 코드를 다음과 같이 수정하면

<span style="font-size: 14px;">    !function () {<br>      function Leaker() {<br>        this.init();<br>      };<br>      Leaker.prototype = {<br>        init: function () {<br>          this.name = (Array(100000)).join('*');<br>        },<br><br>        destroy: function () {<br>          // do something....<br>        }<br>      };<br>      document.querySelector('input').addEventListener('click', function () {<br>        new Leaker();<br>      }, false);<br>    }()<br></span>

console.log("Leaking an object %o: %o", (new Date()), this); 위 단계를 반복하면 분석 결과는 다음과 같습니다.

일반적인 JavaScript 메모리 누수

비교 분석 결과에서 console.log로 출력된 객체는 가비지 컬렉터에서 재활용되지 않음을 알 수 있습니다. 따라서 페이지의 큰 개체는 console.log에 기록하지 않는 것이 가장 좋습니다. 이는 특히 프로덕션 환경에서 페이지의 전체 성능에 영향을 미칠 수 있기 때문입니다. console.log 외에도 console.dir, console.error, console.warn 등과 같은 유사한 문제도 있습니다. 이러한 세부 사항에는 특별한 주의가 필요합니다.

closures(闭包)

当一个函数A返回一个内联函数B,即使函数A执行完,函数B也能访问函数A作用域内的变量,这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。

<span style="font-size: 14px;">function foo(message) {<br>    function closure() {<br>        console.log(message)<br>    };<br>    return closure;<br>}<br><br>// 使用<br>var bar = foo("hello closure!");<br>bar()// 返回 'hello closure!'<br></span>

在函数foo内创建的函数closure对象是不能被回收掉的,因为它被全局变量bar引用,处于一直可访问状态。通过执行bar()可以打印出hello closure!。如果想释放掉可以将bar = null即可。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

实例------>demos/closures.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Closure</title><br></head><br><br><body><br>  <p>不断单击【click】按钮</p><br>  <button id="click_button">Click</button><br>  <script><br>    function f() {<br>      var str = Array(10000).join('#');<br>      var foo = {<br>        name: 'foo'<br>      }<br>      function unused() {<br>        var message = 'it is only a test message';<br>        str = 'unused: ' + str;<br>      }<br>      function getData() {<br>        return 'data';<br>      }<br>      return getData;<br>    }<br><br>    var list = [];<br>    <br>    document.querySelector('#click_button').addEventListener('click', function () {<br>      list.push(f());<br>    }, false);<br>  </script><br></body><br><br></html><br></span>

这里结合Chrome的Devtools->Memory工具进行分析,操作步骤如下:

⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 选中【Record allocation timeline】选项

  2. 执行一次CG

  3. 单击【start】按钮开始记录堆分析

  4. 连续单击【click】按钮十多次

  5. 停止记录堆分析

일반적인 JavaScript 메모리 누수

上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条,过滤出对应新分配的对象:

일반적인 JavaScript 메모리 누수

查看对象的详细信息:

일반적인 JavaScript 메모리 누수

从图可知,在返回的闭包作用链(Scopes)中携带有它所在函数的作用域,作用域中还包含一个str字段。而str字段并没有在返回getData()中使用过。为什么会存在在作用域中,按理应该被GC回收掉, why

原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因,便可以对症下药了。对代码做如下修改:

<span style="font-size: 14px;">    function f() {<br>      var str = Array(10000).join('#');<br>      var foo = {<br>        name: 'foo'<br>      }<br>      function unused() {<br>        var message = 'it is only a test message';<br>        // str = 'unused: ' + str; //删除该条语句<br>      }<br>      function getData() {<br>        return 'data';<br>      }<br>      return getData;<br>    }<br><br>    var list = [];<br>    <br>    document.querySelector('#click_button').addEventListener('click', function () {<br>      list.push(f());<br>    }, false);<br></span>

getData()和unused()内部函数共享f函数对应的变量对象,因为unused()内部函数访问了f作用域内str变量,所以str字段存在于f变量对象中。加上getData()内部函数被返回,被其他对象引用,形成了闭包,因此对应的f变量对象存在于闭包函数的作用域链中。这里只要将函数unused中str = 'unused: ' + str;语句删除便可解决问题。

일반적인 JavaScript 메모리 누수

查看一下闭包信息:

일반적인 JavaScript 메모리 누수

DOM泄露

在JavaScript中,DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。如Chrome浏览器中DOM位于WebCore,而JavaScript/ECMAScript位于V8中。假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大影响。了解更多ℹ️

일반적인 JavaScript 메모리 누수

为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。

实例------>demos/dom.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Dom-Leakage</title><br></head><br><body><br>  <input type="button" value="remove" class="remove"><br>  <input type="button" value="add" class="add"><br><br>  <p class="container"><br>    <pre class="wrapper">