>  기사  >  웹 프론트엔드  >  JavaScript 프로그램의 메모리 누수 그래픽 코드에 대한 자세한 소개

JavaScript 프로그램의 메모리 누수 그래픽 코드에 대한 자세한 소개

黄舟
黄舟원래의
2017-03-09 14:11:011467검색

JavaScript 프로그램의 메모리 누수 그래픽 코드에 대한 자세한 소개:

가비지 수집을 통해 우리는 해방되고 애플리케이션 로직에 집중할 수 있습니다. (메모리 관리가 아님) 그러나 가비지 수집은 마법이 아닙니다. 작동 방식과 오래 전에 해제되어야 하는 메모리를 유지하는 방법을 이해하면 더 빠르고 안정적인 애플리케이션을 만들 수 있습니다. 이 기사에서는 JavaScript 애플리케이션에서 메모리 누수를 찾는 체계적인 접근 방식, 몇 가지 일반적인 누수 패턴 및 이러한 누수를 해결하는 적절한 방법을 알아봅니다.

소개

JavaScript와 같은 스크립팅 언어를 다룰 때 모든 개체, 클래스, 문자열, 숫자 및 메서드가 메모리를 할당하고 예약해야 한다는 사실을 잊기 쉽습니다. 언어 및 런타임 가비지 수집기는 메모리 할당 및 할당 취소에 대한 구체적인 세부 정보를 숨깁니다.

메모리 관리를 고려하지 않고 많은 기능을 구현할 수 있지만 이를 무시하면 프로그램에 심각한 문제가 발생할 수 있습니다. 부적절하게 청소된 개체는 예상보다 훨씬 오래 지속될 수 있습니다. 이러한 개체는 계속해서 이벤트에 응답하고 리소스를 소비합니다. 브라우저가 가상 디스크 드라이브에서 메모리 페이지를 할당하도록 강제하여 컴퓨터 속도를 크게 저하시킵니다. 극단적인 경우에는 브라우저 충돌을 일으킵니다.

메모리 누수는 더 이상 소유하지 않거나 필요하지 않은 후에도 지속되는 객체입니다. 최근 몇 년 동안 많은 브라우저는 페이지 로딩 중에 JavaScript에서 메모리를 회수하는 기능을 개선했습니다. 그러나 모든 브라우저가 동일한 방식으로 작동하는 것은 아닙니다. Firefox와 이전 버전의 Internet Explorer 모두 브라우저가 닫힐 때까지 지속되는 메모리 누수를 경험했습니다.

과거에 메모리 누수를 일으켰던 많은 클래식 패턴이 최신 브라우저에서는 더 이상 메모리 누수를 일으키지 않습니다. 그러나 오늘날 메모리 누수에 영향을 미치는 다른 추세가 있습니다. 많은 사람들이 하드 페이지 새로 고침 없이 단일 페이지에서 실행되도록 웹 애플리케이션을 설계하고 있습니다. 이와 같은 단일 페이지에서는 애플리케이션의 한 상태에서 다른 상태로 이동할 때 더 이상 필요하지 않거나 관련이 없는 메모리를 쉽게 유지할 수 있습니다.

이 기사에서는 객체의 기본 수명 주기, 가비지 수집으로 객체가 해제되었는지 여부를 결정하는 방법, 잠재적인 누수 동작을 평가하는 방법에 대해 알아봅니다. 또한 Google Chrome에서 힙 프로파일러를 사용하여 메모리 문제를 진단하는 방법을 알아보세요. 일부 예에서는 클로저, 콘솔 로그 및 루프로 인한 메모리 누수를 해결하는 방법을 보여줍니다.

객체 수명주기

메모리 누수를 방지하는 방법을 이해하려면 객체의 기본 수명주기를 이해해야 합니다. 객체가 생성되면 JavaScript는 객체에 적절한 메모리를 자동으로 할당합니다. 이 순간부터 가비지 수집기는 개체를 지속적으로 평가하여 해당 개체가 여전히 유효한 개체인지 확인합니다.

가비지 수집기는 주기적으로 개체를 검색하고 각 개체에 대한 참조가 있는 다른 개체의 수를 계산합니다. 객체에 0개의 참조가 있거나(다른 객체가 이를 참조하지 않음) 객체에 대한 유일한 참조가 순환이면 객체의 메모리를 회수할 수 있습니다. 그림 1은 가비지 컬렉터가 메모리를 회수하는 예를 보여줍니다.

그림 1. 가비지 수집을 통한 메모리 회수

展示与各个对象关联的 root 节点的 4 个步骤。

이 시스템이 실제로 작동하는 모습을 보는 것이 도움이 되지만 이 기능을 제공하는 도구는 제한되어 있습니다. JavaScript 애플리케이션이 차지하는 메모리 양을 확인하는 한 가지 방법은 시스템 도구를 사용하여 브라우저의 메모리 할당을 살펴보는 것입니다. 현재 사용량과 시간 경과에 따른 프로세스의 메모리 사용량 추세 그래프를 제공할 수 있는 여러 도구가 있습니다.

예를 들어 Mac OSX에 XCode가 설치되어 있는 경우 Instruments 앱을 실행하고 Activity Monitor 도구를 브라우저에 연결하여 실시간 분석을 수행할 수 있습니다. Windows®에서는 작업 관리자를 사용할 수 있습니다. 애플리케이션을 사용하면서 시간이 지남에 따라 메모리 사용량이 꾸준히 증가하는 것을 발견하면 메모리 누수가 발생한 것입니다.

브라우저의 메모리 사용량을 관찰하면 JavaScript 애플리케이션의 실제 메모리 사용량을 대략적으로 알 수 있을 뿐입니다. 브라우저 데이터는 어떤 개체가 유출되었는지 알려주지 않으며 데이터가 실제로 애플리케이션의 실제 메모리 공간과 일치한다는 보장도 없습니다. 또한 일부 브라우저의 구현 문제로 인해 페이지에서 해당 요소가 소멸될 때 DOM 요소(또는 대체 애플리케이션 수준 개체)가 해제되지 않을 수 있습니다. 이는 브라우저가 보다 정교한 인프라를 구현해야 하는 비디오 태그의 경우 특히 그렇습니다.

클라이언트 측 JavaScript 라이브러리에 메모리 할당 추적을 추가하려는 시도가 여러 번 있었습니다. 불행하게도 그 어떤 시도도 특별히 신뢰할 만한 것은 없었습니다. 예를 들어 인기 있는 stats.js 패키지는 부정확성으로 인해 지원되지 않습니다. 일반적으로 클라이언트에서 이 정보를 유지하거나 확인하려고 하면 응용 프로그램에 오버헤드가 발생하고 안정적으로 종료될 수 없기 때문에 문제가 됩니다.

이상적인 솔루션은 브라우저 공급업체가 브라우저 내에서 메모리 사용량을 모니터링하고, 유출된 개체를 식별하고, 특정 개체가 여전히 예약된 것으로 표시되는 이유를 확인하는 데 도움이 되는 도구 세트를 제공하는 것입니다.

현재 Google Chrome(힙 프로필 제공)만이 메모리 관리 도구를 개발자 도구로 구현하고 있습니다. 이 기사에서는 힙 프로파일러를 사용하여 JavaScript 런타임이 메모리를 처리하는 방법을 테스트하고 시연합니다.

힙 스냅샷 분석

메모리 누수를 생성하기 전에 메모리를 올바르게 수집하는 간단한 상호 작용을 검토하세요. 목록 1과 같이 두 개의 버튼이 있는 간단한 HTML 페이지를 만드는 것부터 시작하세요.

목록 1. index.html

<html>
<head>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" 
type="text/javascript"></script>
</head>
<body>
    <button id="start_button">Start</button>
    <button id="destroy_button">Destroy</button>
    <script src="assets/scripts/leaker.js" type="text/javascript" 
charset="utf-8"></script>
    <script src="assets/scripts/main.js" type="text/javascript" 
charset="utf-8"></script>
</body>
</html>

jQuery를 포함하여 다양한 브라우저에서 작동하는 이벤트 바인딩 관리를 위한 간단한 구문을 보장하고 엄격하게 가장 일반적인 개발 관행을 준수합니다. leaker 클래스와 기본 JavaScript 메서드에 스크립트 태그를 추가합니다. 개발 환경에서는 JavaScript 파일을 단일 파일로 결합하는 것이 더 나은 방법인 경우가 많습니다. 이 예에서는 논리를 별도의 파일에 넣는 것이 더 쉽습니다.

힙 프로파일러를 필터링하여 특정 클래스의 인스턴스만 표시할 수 있습니다. 이 기능을 활용하려면 누수 개체의 동작을 캡슐화하는 새 클래스를 만듭니다. 이 클래스는 목록 2에 표시된 대로 힙 프로파일러에서 쉽게 찾을 수 있습니다.

목록 2. 자산/스크립트/leaker.js

var Leaker = function(){};
Leaker.prototype = {
    init:function(){

    }    
};

시작 버튼을 바인딩하여 Leaker 객체를 초기화하고 대상에 할당 전역 네임스페이스의 변수입니다. 또한 목록 3에 표시된 대로 Leaker 개체를 정리하고 가비지 수집을 위해 준비해야 하는 메서드에 Destroy 버튼을 바인딩해야 합니다.

목록 3. 자산/스크립트/main.js

$("#start_button").click(function(){
    if(leak !== null || leak !== undefined){
        return;
    }
  leak = new Leaker();
  leak.init();
});

$("#destroy_button").click(function(){
    leak = null;
});

var leak = new Leaker();

이제 객체를 생성하고 메모리에서 볼 준비가 되었습니다. 그런 다음 릴리스하십시오.

  1. Chrome에서 색인 페이지를 로드합니다. Google에서 직접 jQuery를 로드하기 때문에 이 샘플을 실행하려면 인터넷 연결이 필요합니다.

  2. 보기 메뉴를 열고 개발 하위 메뉴를 선택하여 개발자 도구를 엽니다. 개발자 도구 명령을 선택하세요.

  3. 프로필 탭으로 이동하여 그림 2와 같이 힙 스냅샷을 찍습니다.

    그림 2. 프로필 탭

    Google Chrome 上的 profiles 选项卡的快照。

  4. 웹으로 돌아와 시작을 선택하세요. .

  5. 또 다른 힙 스냅샷을 찍습니다.

  6. 첫 번째 스냅샷을 필터링하고 Leaker 클래스의 인스턴스를 찾으면 인스턴스가 발견되지 않습니다. 두 번째 스냅샷으로 전환하면 그림 3과 같은 인스턴스를 찾아야 합니다.

    그림 3. 스냅샷 예

    Heap Profiler 过滤器页面的快照

  7. 웹으로 다시 관심을 돌려 삭제를 선택하세요. .

  8. 세 번째 힙 스냅샷을 찍습니다.

  9. 세 번째 스냅샷을 필터링하고 Leaker 클래스의 인스턴스를 찾으면 인스턴스가 발견되지 않습니다. 세 번째 스냅샷을 로드하는 동안 분석 모드를 요약에서 비교로 전환하고 세 번째와 두 번째 스냅샷을 비교할 수도 있습니다. -1의 오프셋 값을 볼 수 있습니다(스냅샷 사이에 Leaker 개체의 인스턴스가 해제됨).

만세! 쓰레기 수거가 효과적입니다. 이제 그것을 파괴할 시간이다.

메모리 누수 1: 클로저

객체가 가비지 수집되는 것을 방지하는 간단한 방법은 콜백에서 객체를 참조하는 간격이나 시간 제한을 설정하는 것입니다. 실제로 작동하는 모습을 보려면 목록 4에 표시된 대로 Leaker.js 클래스를 업데이트하세요.

목록 4. 자산/스크립트/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        this._interval = null;
        this.start();
    },

    start: function(){
        var self = this;
        this._interval = setInterval(function(){
            self.onInterval();
        }, 100);
    },

    destroy: function(){
        if(this._interval !== null){
            clearInterval(this._interval);          
        }
    },

    onInterval: function(){
        console.log("Interval");
    }
};

이제 이전 섹션의 1~9단계를 반복할 때 다음을 수행해야 합니다. 세 번째 스냅샷에서 Leaker 객체가 지속되고 간격이 영원히 계속 실행되는 것을 확인하세요. 그래서 무슨 일이 일어났나요? 클로저 내에서 참조되는 모든 지역 변수는 클로저가 존재하는 한 클로저에 의해 유지됩니다. Leaker 인스턴스의 범위에 액세스할 때 setInterval 메서드에 대한 콜백이 실행되도록 하려면 this 변수를 내부에서 self를 트리거하는 데 사용되는 로컬 변수 onInterval에 할당해야 합니다. 폐쇄. onInterval가 실행되면 Leaker 객체(자신 포함)의 모든 인스턴스 변수에 액세스할 수 있습니다. 그러나 이벤트 리스너가 존재하는 한 Leaker 객체는 가비지 수집되지 않습니다.

이 문제를 해결하려면 목록 5에 표시된 것처럼 Destroy 버튼의 클릭 핸들러를 업데이트하여 삭제하기 전에 저장된 leaker 개체 참조에 추가된 destroy 메서드를 트리거하세요.

목록 5. 자산/스크립트/main.js

$("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
});

销毁对象和对象所有权

一种不错的做法是,创建一个标准方法来负责让一个对象有资格被垃圾回收。destroy 功能的主要用途是,集中清理该对象完成的具有以下后果的操作的职责:

  • 阻止它的引用计数下降到 0(例如,删除存在问题的事件侦听器和回调,并从任何服务取消注册)。

  • 使用不必要的 CPU 周期,比如间隔或动画。

destroy 方法常常是清理一个对象的必要步骤,但在大多数情况下它还不够。在理论上,在销毁相关实例后,保留对已销毁对象的引用的其他对象可调用自身之上的方法。因为这种情形可能会产生不可预测的结果,所以仅在对象即将无用时调用 destroy 方法,这至关重要。

一般而言,destroy 方法最佳使用是在一个对象有一个明确的所有者来负责它的生命周期时。此情形常常存在于分层系统中,比如 MVC 框架中的视图或控制器,或者一个画布呈现系统的场景图。

内存泄漏 2:控制台日志

一种将对象保留在内存中的不太明显的方式是将它记录到控制台中。清单 6 更新了 Leaker 类,显示了此方式的一个示例。

清单 6. assets/scripts/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        console.log("Leaking an object: %o", this);
    },

    destroy: function(){

    }      
};

可采取以下步骤来演示控制台的影响。

  1. 登录到索引页面。

  2. 单击 Start

  3. 转到控制台并确认 Leaking 对象已被跟踪。

  4. 单击 Destroy

  5. 回到控制台并键入 leak,以记录全局变量当前的内容。此刻该值应为空。

  6. 获取另一个堆快照并过滤 Leaker 对象。您应留下一个 Leaker 对象。

  7. 回到控制台并清除它。

  8. 创建另一个堆配置文件。在清理控制台后,保留 leaker 的配置文件应已清除。

控制台日志记录对总体内存配置文件的影响可能是许多开发人员都未想到的极其重大的问题。记录错误的对象可以将大量数据保留在内存中。注意,这也适用于:

  • 在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。

  • 由 console.log 和 console.dir 方法记录的对象。

内存泄漏 3:循环

在两个对象彼此引用且彼此保留时,就会产生一个循环,如图 4 所示。

图 4. 创建一个循环的引用

该图中的一个蓝色 root 节点连接到两个绿色框,显示了它们之间的一个连接

清单 7 显示了一个简单的代码示例。

清单 7. assets/scripts/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(name, parent){
        this._name = name;
        this._parent = parent;
        this._child = null;
        this.createChildren();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create a child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this);
    },

    destroy: function(){

    }
};

Root 对象的实例化可以修改,如清单 8 所示。

清单 8. assets/scripts/main.js

leak = new Leaker(); 
leak.init("leaker 1", null);

如果在创建和销毁对象后执行一次堆分析,您应该会看到垃圾收集器检测到了这个循环引用,并在您选择 Destroy 按钮时释放了内存。

但是,如果引入了第三个保留该子对象的对象,该循环会导致内存泄漏。例如,创建一个 registry 对象,如清单 9 所示。

清单 9. assets/scripts/registry.js

var Registry = function(){};

Registry.prototype = {
    init:function(){
        this._subscribers = [];
    },

    add:function(subscriber){
        if(this._subscribers.indexOf(subscriber) >= 0){
            // Already registered so bail out
            return;
        }
        this._subscribers.push(subscriber);
    },

    remove:function(subscriber){
        if(this._subscribers.indexOf(subscriber) < 0){
            // Not currently registered so bail out
            return;
        }
              this._subscribers.splice(
                  this._subscribers.indexOf(subscriber), 1
              );
    }
};

registry 类是让其他对象向它注册,然后从注册表中删除自身的对象的简单示例。尽管这个特殊的类与注册表毫无关联,但这是事件调度程序和通知系统中的一种常见模式。

将该类导入 index.html 页面中,放在 leaker.js 之前,如清单 10 所示。

清单 10. index.html

<script src="assets/scripts/registry.js" type="text/javascript" 
charset="utf-8"></script>

更新 Leaker 对象,以向注册表对象注册该对象本身(可能用于有关一些未实现事件的通知)。这创建了一个来自要保留的 leaker 子对象的 root 节点备用路径,但由于该循环,父对象也将保留,如清单 11 所示。

清单 11. assets/scripts/leaker.js

var Leaker = function(){};
Leaker.prototype = {

    init:function(name, parent, registry){
        this._name = name;
        this._registry = registry;
        this._parent = parent;
        this._child = null;
        this.createChildren();
        this.registerCallback();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this, this._registry);
    },

    registerCallback:function(){
        this._registry.add(this);
    },

    destroy: function(){
        this._registry.remove(this);
    }
};

最后,更新 main.js 以设置注册表,并将对注册表的一个引用传递给 leaker 父对象,如清单 12 所示。

清单 12. assets/scripts/main.js

	  $("#start_button").click(function(){
  var leakExists = !(
	      window["leak"] === null || window["leak"] === undefined
	  );
  if(leakExists){
      return;
  }
  leak = new Leaker();
  leak.init("leaker 1", null, registry);
});

$("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
});

registry = new Registry();
registry.init();

现在,当执行堆分析时,您应看到每次选择 Start 按钮时,会创建并保留 Leaker 对象的两个新实例。图 5 显示了对象引用的流程。

图 5. 由于保留引用导致的内存泄漏

3 个方框显示了 root 节点与父和子对象之间的 3 个不同路径

从表面上看,它像一个不自然的示例,但它实际上非常常见。更加经典的面向对象框架中的事件侦听器常常遵循类似图 5 的模式。这种类型的模式也可能与闭包和控制台日志导致的问题相关联。

尽管有多种方式来解决此类问题,但在此情况下,最简单的方式是更新 Leaker 类,以在销毁它时销毁它的子对象。对于本示例,更新destroy 方法(如清单 13 所示)就足够了。

清单 13. assets/scripts/leaker.js

destroy: function(){
    if(this._child !== null){
        this._child.destroy();            
    }
    this._registry.remove(this);
}

有时,两个没有足够紧密关系的对象之间也会存在循环,其中一个对象管理另一个对象的生命周期。在这样的情况下,在这两个对象之间建立关系的对象应负责在自己被销毁时中断循环。

结束语

即使 JavaScript 已被垃圾回收,仍然会有许多方式会将不需要的对象保留在内存中。目前大部分浏览器都已改进了内存清理功能,但评估您应用程序内存堆的工具仍然有限(除了使用 Google Chrome)。通过从简单的测试案例开始,很容易评估潜在的泄漏行为并确定是否存在泄漏。

不经过测试,就不可能准确度量内存使用。很容易使循环引用占据对象曲线图中的大部分区域。Chrome 的 Heap Profiler 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。

在创建对象时要计划该对象的清理工作,这比在以后将一个清理阶段移植到应用程序中要容易得多。常常要计划删除事件侦听器,并停止您创建的间隔。如果认识到了您应用程序中的内存使用,您将得到更可靠且性能更高的应用程序。


위 내용은 JavaScript 프로그램의 메모리 누수 그래픽 코드에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.