4

什么是内存泄漏

简介

CPU,内存,硬盘的关系

CPU(Central Processing Unit)工作的时候:
  1、需要从存储器里取数据出来。
  2、进行运算,要不停地用存储器读写。
  3、计算出结果再返回到存储器里。
举例子形容关系
图片描述
我们的PC的APP,手机的APP都是跑在内存上的。
程序的运行需要内存。只要程序提出要求,操作系统就必须供给内存。

那么什么是内存呢?

图片描述

内存就是处于外存和CPU之间的桥梁,用于存储CPU的运算数据,这样内存就可以保持记忆功能,你写的所有代码,都是需要在内存上跑的,虚拟内存是从外存上分配的,很慢
内存的频率(mhz)越高代表内存运算更快,同一块内存我跑的更快哟,这就是为什么DDR5比DDR3快的原因
说这个的原因,就是如果我的计算机性能足够好的话,内存泄漏带来的问题就会越来越小。

那么什么是内存溢出呢?

out of memory

内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,就会出现内存溢出。

在手机上,比如任何一个app,系统初始的时候可能只会给你分配100m的内存,如果有android studio的话,可以在log上看到,这个时候你点击了某个图片列表页(为什么用图片举例,是因为图片特有的情况,图片本身如果是20Kb,长宽为300的话,渲染到手机上由于图片采用的ARGB-888色彩格式,每个像素点占用4个字节(双通道),这样图片实际占用内存就是3003004/1024/1024 = 300+k dpi为1的情况),这个时候内存就会暴涨,一旦接近临界值,程序就会去找操作系统说,我内存不够了,再给我点,系统就会又给你分配一段,完了你返回首页了,但是因为你的代码写的有问题,暴露各种全局对象啊,各种监听啊,一进一出多次,但是系统给每个app分配的内存是有上限的,直到内存不够分,泄漏导致的内存溢出。然后crash掉。以前我写rn的时候,早期的scrollview性能堪忧,出现过内存溢出的现象。

内存泄漏

memory leak

内存泄漏指的是你申请了一块内存,在使用后无法释放已申请的内存空间,比如程序会认为你可能会用到这个变量,就一直给你留着不释放,一次内存泄漏可以被忽略,但是内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

既然内存我可以申请,就可以被系统回收,在C语言中,需要程序员手动malloc去申请内存,然后free掉它,这写起来很麻烦,所以其他大多数语言都提供了自动回收的机制,那么既然自动回收了,就很容易出现各种问题。

内存泄漏的后果

通常来说问题并不是特别大,因为正常一个进程的生命周期有限,在当下的大内存快cpu的手机下,影响有限,不过还是要列举一些情况。
1:安卓手机内存管理不好,导致只要不重启,时间越长,可用内存越少,即使杀程序。具体缘由可能还和安卓开放过多权限导致无良app各种保持后台后门运行也有一定关系。
2:导致内存溢出,如果手机内存被挤占的有限,那么手机会变卡,严重的自己crash掉,如果是pc端,浏览器的内存泄漏导致的溢出会让浏览器出现假死状态,只能通过强制关闭解决,如果是在webview上,比如我开始的时候写过一个代码在ios微信浏览器上调用swiper 的3d变换导致微信直接闪退。
3:以上还是客户端的,客户端大多数情况下不会停留时间过长,所以除非是非常规操作,很少会出大问题,但是,跑在服务端的程序,通常都是一直跑几天甚至是几个月的,如果这个里面有内存泄漏引发的内存溢出的话,那么就会导致服务器宕机,必须重启。那带来的损失就很大了。

引发内存泄漏的方式

1.意外的全局变量

JavaScript 对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过delete删除)。如果在浏览器中,全局对象就是window对象。
如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。

那么为什么会对未声明的变量处理方式是挂window下呢?
“当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下”

摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)。” iBooks.
function foo(arg) {
  bar = 'this is hidden global variable';
}

等同于:

function foo(arg) {
  window.bar = 'this is hidden global variable';
}

另外,通过this创建意外的全局变量:

function foo() {
  this.variable = 'this is hidden global variable';
}
// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'
foo();

------------->演示

解决方案

正常的定义全局变量没有问题,但是这种是属于意外的泄漏,所以可以使用严格模式处理,规范自己的代码。

2.console.log

传递给console.log的对象是不能被垃圾回收 ♻️,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。
追踪线上问题,console绝非是个好的方式。因为发生问题一般在用户哪里,你没办法看用户的日志。

function aaa() {
    this.name = (Array(100000)).join('*');
    console.log(this);
}
document.getElementsByClassName('console-obj')[0].addEventListener('click', function () {
      var oo = new aaa();
});

------------->演示

解决方案

可以删除自己的console.log,但是显然,在开发环境下,我就是想看我的console.log,这样注释来注释去也挺麻烦的,所以可以判断下当前的环境是不是env,如果是product环境下的话,直接

window.console.log = function(){return 'warn:do not use my log'}

这样的手法不仅可以屏蔽console.log,还能防止别人在我们的页面下console.log调试

延伸:如何保护自己的页面安全

3.闭包(closures)

由于闭包的特性,通过闭包而能被访问到的变量,显然不会被内存回收♻️,因为被回收的话就没闭包了这个概念了。

    function foo() {
      var str = Array(10000).join('#');
      var msg = "test message";
      function unused() {
        var message = 'it is only a test message';
        str = 'unused: ' + str;
      }
      function getData() {
          return msg;
      }
      return getData;
    }
    var bar;
    document.getElementsByClassName('closure-obj')[0].addEventListener('click', function () {
        bar = foo();
    });
    // var list = [];
    // document.getElementsByClassName('closure-obj')[0].addEventListener('click', function () {
    //     list.push(foo());
    // });
  • 演示内存performance情况
  • 演示memory 情况
  • 断点演示闭包scope,call stack

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

延伸:VO/AO,call stack

解决方案

不暴露到全局变量上,这样就不会有问题,暴露到全局变量上就手动置为null,垃圾回收器下次回来会带走它

4.dom泄漏

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

为了减少 DOM 访问次数,一般情况下,当需要多次访问同一个 DOM 方法或属性时,会将 DOM 引用缓存到一个局部变量中。

但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的 DOM 引用,这样会造成 DOM 内存泄露。

    <input type="button" value="remove" class="remove" style="display:none;">
  <input type="button" value="add" class="add">
  <div class="container">
    <ul class="wrapper"></ul>
  </div>
    // 因为要多次用到pre.wrapper、div.container、input.remove、input.add节点,将其缓存到本地变量中,
      var wrapper = document.querySelector('.wrapper');
      var container = document.querySelector('.container');
      var removeBtn = document.querySelector('.remove');
      var addBtn = document.querySelector('.add');
      var counter = 0;
      var once = true;
      // 方法
      var hide = function(target){
        target.style.display = 'none';
      }
      var show = function(target){
        target.style.display = 'inline-block';
      }
      // 回调函数
      var removeCallback = function(){
        removeBtn.removeEventListener('click', removeCallback, false);
        addBtn.removeEventListener('click', addCallback, false);
        hide(addBtn);
        hide(removeBtn);
        container.removeChild(wrapper);
        wrapper = null;
      }
      var addCallback = function(){
        let p = document.createElement('li');
        p.appendChild(document.createTextNode("+ ++counter + ':a new line text\n"));
        wrapper.appendChild(p);
        // 显示删除操作按钮
        if(once){
          show(removeBtn);
          once = false;
        }
      }
      // 绑定事件
      removeBtn.addEventListener('click', removeCallback, false);
      addBtn.addEventListener('click', addCallback, false);

--------->演示代码

    var refA = document.getElementById('refA');
    var refB = document.getElementById('refB');
    document.body.removeChild(refA);

    // #refA不能GC回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。
    refA = null;

    // 还存在变量refB对#refA的间接引用(refB引用了#refB,而#refB属于#refA)。将变量refB对#refB的引用释放,#refA就可以被GC回收。
    refB = null;

图片描述

5.计时器/监听器

var counter = 0;
    var clock = {
      start: function () {
        // setInterval(this.step, 1000);
        if(!this.timer){
          this.timer = setInterval(this.step, 1000);
        }
      },
      step: function () {
        var date = new Date();
        var h = date.getHours();
        var m = date.getMinutes();
        var s = date.getSeconds();
        console.log('step running');
      }
    }
    // function goo(){
    //     // clock = null;
    //     clearInterval(clock.timer);
    //     console.log('click stop');
    // }
    document.querySelector('.start').addEventListener('click', function () {
      clock.start();
      // document.querySelector('.stop').addEventListener('click',);
    });
    document.querySelector('.stop').addEventListener('click', function () {
      // clock = null;
      clearInterval(clock.timer);
    });

监听器没有及时回收或者是匿名回收导致的。
bind,call,apply的区别

如何使用chrome performance

  1. 开启【Performance】项的记录
  2. 执行一次 CG,创建基准参考线
  3. 操作页面
  4. 执行一次 CG
  5. 停止记录

以上就是我们使用的时候的步骤
那么对这个performances里的各项是如何理解的呢?

前置问题1:什么是回流,什么是重绘,以及为什么回流一定会导致重绘,但是重绘不会导致回流?

中置问题2:浏览器到了渲染阶段的过程是什么?
图片描述

一次性能的记录就完整的展示的浏览器的渲染全过程。从图中也可以看出,layout后的阶段是Painting

跑一个performances

Performances 各项简介

  • FPS 每秒的帧数,绿色条约稿,表示FPS值越高,通常上面附带红色块的帧表示该帧时间过长,可能需要优化。
  • CPU CPU资源,面积图表示不同事件对CPU资源的消耗。
  • NET 这个项和以前的不一样,查询相关资料也没有找到到底显示的是什么,所以只能通过下面的具体来看,HTML文件是蓝色条,脚本文件是黄色条,样式文件是紫色条,媒体文件是绿色条,其他的是灰色条,网络请求部分更详细的信息建议查看Network。
  • HEAP 内存占用情况
  • 三条虚线:蓝色指DOMConentLoaded,绿线表示第一次绘制,红线表示load事件,很明显看到load是比较慢的。
  • summary loading代表html花的时间,scripting代表脚本的时间,rendering代表计算样式和回流花的时间,painting代表绘制的时间
  • Bottom-up 代表花费排序
  • call-tree 代表调用排序
  • event-log 代表各项事务时间线

重点看看这个event-log,以回流为例子,再次确认回流后跟着painting,看看有哪些回流,然后去看看时间节点,发现对应的页面出现。
回流操作还是挺占用时间的
以拼团列表图片高度加载导致的回流问题,可以用一个object-fit来搞定常见的情况

如何规避内存泄漏

注意代码规范,注意代码规范,注意代码规范

垃圾回收

讲讲垃圾回收,说白了,内存泄漏,溢出,就是因为js有自动垃圾回收的机制,然后自动的垃圾回收器并不能准确的回收你所不想用的东西,就会出一些问题,那么常见的垃圾回收有两种

引用计数

当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。 如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这 个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。

//赋值给o1的对象a{},赋值给o2的对象b{};
var o1 = {
  o2: {
    x: 1
  }
};
//a+1 = 1,b作为属性也+1 = 1;
var o3 = o1;
//a+1+1 = 2,b+1+1 = 2                                                 
o1 = 1;     
//a+1+1-1 = 1,b+1+1-1 = 1;
var o4 = o3.o2;
//a+1+1-1 = 1,b+1+1-1+1 = 2;
o3 = '374'; 
//a+1+1-1-1 = 0,b+1+1-1+1-1 = 1;
o4 = null; 
//b-1 = 0;

循环引用导致的问题

//o1:x{},o2:y{};
function f() {
  var o1 = {};
   //x+1 = 1;
  var o2 = {};
    //y+1 = 1;
  o1.p = o2; // o1 references o2
    //y+1+1 = 2;
  o2.p = o1; // o2 references o1. This creates a cycle.
    //x+1+1 = 2;
}
f();

图片描述

这段代码o1和o2互相引用导致引用次数回收的时候不为1,就没有办法回收。
假设没有o2.p= o1这段,那么o1在出函数的时候要给对应的对象减一,结果发现,o1有一个属性p还没解除引用,所以先去解o1.p的,这个时候o2的对象就减一次,完了后o1.p就没了,那o1就可以解除o1的对象,o2再-它自己的,都为0,没泄漏

反过来,如果上了那段代码的话,o1要解除,先走p,o1.p想解除,结果发现o2有个p,又去解o2.p,死循环,一个都解不了,还是2.

假如这个函数被重复多次调用,就会导致大量内存得 不到回收。为此,Netscape 在 Navigator 4.0 中放弃了引用计数方式,转而采用标记清除来实现其垃圾收 集机制。可是,引用计数导致的麻烦并未就此终结。到目前为止,几乎所有的浏览器都是使用的标记清楚策略,只不过垃圾收集的时间间隔稍微不同。

标记清除

当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其 标记为“离开环境”。

Mark and sweep

过去几年,JavaScript 垃圾回收(代数、增量、并行、并行垃圾收集)领域的所有改进都是对该算法(mark-and-sweep)的实现进行改进,但并没有对垃圾回收算法本身进行改进,其目标是确定一个对象是否可达。
图片描述
这样的话,循环引用将不再是问题
图片描述
尽管两个对象还是存在引用,但是他们从 root 出发已经是不可达的了。

总结

在Javascript中,彻底避免垃圾回收或者是内存泄漏是非常困难的。所以我们能做的就是减少泄漏,减少垃圾回收的频率。对一些高频使用的函数之类的东西去做一些类似的优化。综合考虑优化成本


jansen
130 声望16 粉丝

学习不能止步,学习就是兴趣!终生学习是目标


下一篇 »
观察者们