如何在Angular应用程序中捕捉内存泄漏?

39

我有一个用AngularJS编写的Web应用程序,基本上轮询两个API端点。所以,每分钟它轮询一次,看是否有任何新内容。

我发现它存在一个小内存泄漏,尽管我已经尽力找到它,但我无法解决它。在这个过程中,我成功地减少了我的应用程序的内存使用量,这非常好。

没有做任何其他事情,每次轮询都会看到内存使用量的暴增(这是正常的),然后它应该下降,但它总是在增加。我将数组的清理从[]更改为array.length = 0,我确信引用不会持久存在,因此不应该保留其中任何内容。

我还尝试了这个:https://github.com/angular/angular.js/issues/1522

但是没有任何运气...

因此,这是两个堆之间的比较:

Memory heap

大部分泄漏似乎来自于(array),如果我打开它们,这些数组是API调用解析返回的,但我确信它们没有被存储:

这基本上是结构:

poll: function(service) {
  var self = this;
  log('Polling for %s', service);

  this[service].get().then(function(response) {
    if (!response) {
      return;
    }

    var interval = response.headers ? (parseInt(response.headers('X-Poll-Interval'), 10) || 60) : 60;

    services[service].timeout = setTimeout(function(){
      $rootScope.$apply(function(){
        self.poll(service);
      });
    }, interval * 1000);

    services[service].lastRead = new Date();
    $rootScope.$broadcast('api.'+service, response.data);
  });
}

基本上,假设我有一个sellings服务,那么它将是service变量的值。

然后,在主视图中:

$scope.$on('api.sellings', function(event, data) {
  $scope.sellings.length = 0;
  $scope.sellings = data;
});

而且视图确实有一个ngRepeat来根据需要呈现这个。我自己花了很多时间试图弄清楚这个问题,但我做不到。我知道这是一个难题,但是,是否有人有任何想法如何追踪此问题?

编辑1 - 添加 Promise 演示:

这是makeRequest函数,它是这两个服务使用的函数:

return $http(options).then(function(response) {
    if (response.data.message) {
      log('api.error', response.data);
    }

    if (response.data.message == 'Server Error') {    
      return $q.reject();
    }

    if (response.data.message == 'Bad credentials' || response.data.message == 'Maximum number of login attempts exceeded') {
      $rootScope.$broadcast('api.unauthorized');
      return $q.reject();
    }

    return response;
    }, function(response) {
    if (response.status == 401 || response.status == 403) {
      $rootScope.$broadcast('api.unauthorized');
    }
});

如果我注释掉$scope.$on('api.sellings')这部分,内存泄漏仍然存在,但下降到1%。

PS:我正在使用截至目前最新版本的Angular

编辑2 - 在图像中打开(数组)树

在此输入图片描述

它看起来就像这样,所以我觉得它相当无用 :(

此外,以下是4个堆报告,供您自己玩耍:

https://www.dropbox.com/s/ys3fxyewgdanw5c/Heap.zip

编辑3 - 回应 @zeroflagL

尽管闭包部分似乎更好,因为它没有显示jQuery缓存内容,但修改指令对泄漏没有任何影响?

没有泄漏了?

现在指令看起来像这样

var destroy = function(){
  if (cls){
    stopObserving();
    cls.destroy();
    cls = null;
  }
};

el.on('$destroy', destroy);
scope.$on('$destroy', destroy);
对我来说,似乎正在发生的事情与 (array) 相关。在轮询之间还有 3 个新堆

完成 @PieterHerroelen 我还添加了堆报告 - Antonio Laguna
难道是因为您递归调用了 'poll()' 吗?如果在 $broadcast() 之后将 response 设置为 null,会发生什么?这种递归不会导致堆栈溢出吗? - Pieter Herroelen
为什么递归会导致堆栈溢出?一分钟之间有一个空格。你是在暗示无限闭包吗?不管怎样,我已经尝试在广播后将响应设置为null,结果仍然相似... - Antonio Laguna
3
你能在 Plunker/Fiddle 上重现这个吗? - Beyers
+1 @Beyers 这个问题无法通过你提供的信息解决...是哪个服务拥有投票功能?你也没有处理错误情况? - Nix
显示剩余10条评论
2个回答

28

答案是缓存。

堆快照分析

我不知道它是什么,但这个东西会增长。它似乎与jQuery有关。也许它是jQuery元素缓存。你是否碰巧在每次服务调用后对一个或多个元素应用了jQuery插件?

更新

问题在于HTML元素被添加、用jQuery处理(例如通过popbox插件),但要么根本没有被删除,要么没有使用jQuery删除。在这种情况下进行处理意味着像添加事件处理程序这样的操作。缓存对象中的条目(无论是什么)只有在jQuery知道元素已被删除时才会被删除。也就是说,必须使用jQuery删除元素。

更新2

不太清楚为什么这些缓存条目没有被删除,因为angular应该在包含它时使用jQuery。但它们是通过评论中提到的插件添加的,并包含事件处理程序和数据。据我所知,安东尼奥已经更改了插件代码,在插件的destroy()方法中解除了事件处理程序并删除了数据。最终移除了内存泄漏。


1
@AntonioLaguna var $el = $(el); - el 应该已经是一个 jQuery 对象了。也许这就是问题所在。尝试直接使用 el - a better oliver
看起来不是这样。如果我移除它,插件会抱怨说[Object object]没有popbox方法。 - Antonio Laguna
@AntonioLaguna 是的,它只是一个 jqLite 对象,我认为这真的很糟糕。我想我找到了根本原因。我更新了我的答案。 - a better oliver
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/43947/discussion-between-antonio-laguna-and-zeroflagl - Antonio Laguna
@AntonioLaguna zeroflagL是正确的,您不应该使用jQuery包装传递到链接函数中的元素,因为它已经是一个jQuery对象。文档对此非常清楚http://docs.angularjs.org/api/angular.element,还可以参见这个流行答案的第5点https://dev59.com/YGUp5IYBdhLWcg3w3KXe#15012542。您需要做的唯一一件事是在angularjs之前加载jQuery。 - Beyers
显示剩余4条评论

3
标准浏览器解决内存泄漏的方法是刷新页面。由于JavaScript垃圾回收机制通常比较懒惰,因此可能会依赖这一点。由于Angular通常是单页应用程序(SPA),所以浏览器永远没有机会进行刷新。
但我们有一个优势:Javascript主要是一种自上而下的分层语言。我们可以尝试从上到下清除内存泄漏,而不是自下而上地搜索。 因此,我想出了这个解决方案,它有效,但取决于您的应用程序,可能不会100%有效。 主页 典型的Angular应用程序主页包括一些控制器和ng-view。例如: <div ng-controller="MainController as vm"> <div id="main-content-app" ng-view></div> </div> 控制器 然后,在控制器中“刷新”应用程序(在上面的代码中是MainController),我们会重复调用jQuery的.empty()和Angular的.empty(),以确保清除任何跨库引用。
function refreshApp() {
var host = document.getElementById('main-content-app');
if(host) {
    var mainDiv = $("#main-content-app");
    mainDiv.empty();
    angular.element(host).empty();
}
}

在路由开始之前调用上述方法,模拟页面刷新:

$rootScope.$on('$routeChangeStart',
function (event, next, current) {
    refreshApp();
}
);

结果

这种方法有点笨拙,相当于“刷新浏览器”行为,清除DOM和希望的任何泄漏。希望能有所帮助。


3
我欣赏你在回答和编写代码方面所付出的努力,但我认为这种做法(正如你所说)有些hacky。你正确地指出浏览器消除泄漏的方法是刷新页面。然而作为开发者,我们必须创建不会泄漏的代码。现在随着SPA越来越流行,页面可能永远不会刷新,应用程序必须在不对浏览器造成破坏的情况下运行。 - Antonio Laguna
1
我完全同意,但我几乎可以保证随着应用程序变得越来越大和要求更高,缓存的构建等问题将成为一个问题。而且问题不总是出在代码中,而是在浏览器的垃圾回收机制上!过去两周,我所在公司一直在测试一个庞大的Angular应用程序,它在浏览器中的数据量约为300MB(作为测试)。目前,IE 11的垃圾回收机制处理效果最佳。 - jmbmage

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接