性能分析-内存泄漏

一、概述

内存泄漏

        内存泄漏是指在程序运行过程中,当不在需要占用某块内存时,因某些原因,内存没有被正确释放,从而导致内存占用不断增加,可用内存不断减少,最终导致内存耗尽。内存泄漏通常是由于程序中存在未释放的动态内存、循环引用、资源未关闭等因素引起的。

垃圾内存

        垃圾内存是指程序运行过程中,已经不在被使用的内存空间,由于某些原因没有被操作系统或可用内存池及时回收。

        垃圾内存是指没有被引用或无法访问的对象或变量,如果一个对象或变量不再被使用,但仍可被访问,那么它不会被视为垃圾内存。

        例如,一个被定义在函数内部的变量,即使函数执行结束,但该变量仍可被其他函数访问到,所以它并不是垃圾内存。但是如果一个变量或对象被定义在函数内部,且没有被返回或赋值给其他变量,那么该变量或对象将将被视为垃圾内存。

判断垃圾内存

        JS中,window 是最顶级的对象,有人说 window 不可达的变量即为垃圾内存,这种说法是不正确的。不是所有 window 不可达的变量都是垃圾内存。在JS中,垃圾内存是指不再被程序使用的内存空间,可以被垃圾回收机制回收。而 window 不可达的变量只是其中一种情况,不一定就代表这些变量是垃圾内存。

        JS中,只有当一个对象没有任何引用指向它时,才会被判定为垃圾内存。如果一个变量虽不再被 window 对象引用,但在其他地方仍被引用,那么它就不是垃圾内存,不会被垃圾回收机制回收。另外,JS中的垃圾回收机制,是由JS引擎实现的,不同的引擎会采用不同的垃圾回收策略,引擎会根据自身的垃圾回收算法来判断哪些内存空间是垃圾内存,哪些内存空间可以被回收。

        如下是被判定为垃圾内存的三种情况。

        对象没有被引用:如果一个对象没有任何引用指向它,那么它就不再被程序使用,可以被回收。

        对象之间形成了循环引用:如果两个或多个对象形成了循环引用,即相互引用,而且这些对象都没有被外部对象引用,那么它们都可以被回收。

        函数执行完毕后,局部变量的内存空间:函数执行完毕后,局部变量的内存空间会被释放,可以被回收。

二、排查内存泄漏

1、Memory内存分析工具

        Chrome Devtools 中的 Memory 工具可以用来分析内存,主要显示页面JS对象和相关联的DOM节点的内存分布情况。

        如下图红色箭头所示,左上角有三个按钮,分别是开始记录、清除记录和垃圾回收。

        如上图黄色框选所示,Memory工具有三种分析类型。按顺序依次是:

Heap snapshot(堆快照)
Allocation instrumentation on timeline(时间线上的分配检测)
Allocation sampling(分配采用)

        Heap snapshot 翻译为堆快照,用于打印堆快照,可以显示页面的JS对象和相关DOM节点之间的内存分配。

        Allocation instrumentation on timeline 翻译为时间线上的分配检测,可以显示一段时间内检测到的 JS 内存分配。使用此分析类型,可以用来确定内存泄漏。

        Allocation sampling 翻译为分配采样,使用采样方法记录内存分配,此分析类型具有最小的性能开销,可用于长时间运行的操作。它提供了通过 JS 执行堆栈细分的良好分配近似值。

2、Performance页面性能分析工具

        Chrome Devtools 中的 Performance 工具可以用来分析网页的性能,包括加载时间、DOM渲染时间、脚本执行时间等。如下图所示,标出来几个常用的地方。

三、常见的内存泄漏

意料之外的全局变量

        如下代码所示,函数内无任何关键字直接声明的变量会自动挂在到 window 上。

function fn() {
    msg = "hello, world"
}
// 等同于
function fn() {
    window.msg = "hello, world"
}

        如果需要设置全局变量,推荐在 window上明确声明。全局变量完成使命后,及时设置为空或重新赋值。

打印语句

        打印语句 console 可以向控制台打印一条信息,常用于开发时调试分析。经测试发现,使用打印语句会造成内存占用增加。

        如下代码所示,小编带大家测试下打印语句是否会对内存占用造成影响。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>测试GC</title></title>
</head>
<body>
    <div>测试垃圾回收</div>
    <button onclick="handleClick()">点击</button>
    <script>      
        function handleClick() {
            let obj = { name: "JIOA" }
            let arr = new Array(10000)
            console.log(obj, arr)
        }
    </script>
</body>
</html>

        实验步骤如下所示。

第一步:开启【Performance】项的记录
第二步:立即执行一次GC,创建参考的基准线,用于记录初始的内存占用情况
第三步:连续点击按钮【点击】三次
第四步:再执行一次GC
停止记录

        如上图所示,在连续点击三次后,执行GC,内存占用并没有恢复到基准参考线的水平,可以证明,执行 console 打印语句确实会对内存占用造成负面影响。

        尤其,在一些特定情况下,可能会导致严重的内存泄漏,例如:

        循环中使用打印语句,如果在循环中使用 console.log 语句,会导致大量的日志信息被输出到控制台,会占用大量系统资源,可能会导致内存泄漏。

        闭包中使用打印语句,如果在闭包中使用 console.log 语句,会导致闭包中的变量无法被垃圾回收机制回收,从而导致内存泄漏。

        因此,在开发中,打印语句要及时删除,尤其避免在循环或闭包中使用 console.log 语句,以避免因为这些语句导致内存泄漏。

闭包

        把内部函数保存到外部就会形成闭包,闭包是函数内部和外部连接的桥梁,它继承了外部函数的作用域。闭包存在时,即使外部函数被销毁,外部函数的作用域链依旧不会释放。因为闭包使外层函数作用域链不释放,因此使用不当易造成内存泄漏。

        如下代码所示,handleClick函数执行完毕后销毁,但其内部函数被全局变量getName引用,导致其作用域链得不到释放,内存会一直被占用。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>测试GC</title></title>
</head>
<body>
    <div>测试垃圾回收</div>
    <button onclick="handleClick()">点击</button>
    <script>      
        function handleClick() {
            let name = "小明"
            function fn() {
                return name
            }
        }
        let getName = handleClick()        
    </script>
</body>
</html>

        如下图所示,先GC,再点击按钮触发示例代码,再GC后,内存并未降至最初GC的水平。

        如下代码所示,一函数内部定义两个内部变量,两个内部函数,且有一内部函数被 return 语句返回。

<button onclick="handleClick()">Click1</button>
<script>
    function fn() {
        var str1 = "world"
        var str2 = "123"
        function demo1() {
            str1 = "hello" + str1
        }
        function demo2() {
            return "only one sentence"
        }
        return demo2
    }
    function handleClick() {
        console.log("测试", [fn()])
    }
</script>

        点击 click 按钮,打印结果如下图所示。如下红框所示为打印结果的作用域链,有两级,分别是GO和AO,其中AO中只有str1,但没有str2。这是因为str2没有被任何内部函数使用过,所以被回收了,反之,只要str2被任一内部函数使用,则AO中就会保留str2。

DOM泄漏

        JS中频繁的DOM操作是比较耗费性能的。如下为研究DOM泄漏的代码示例。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>测试DOM泄漏</title></title>
</head>
<body>
    <button id="remove">remove</button>
    <button id="add">add</button>
    <div id="list"></div>
    <script>
        let removeBtn = document.getElementById("remove")
        let addBtn = document.getElementById("add")
        let nodeLt = document.getElementById("list")

        addBtn.onclick = () => {
            nodeLt.appendChild(document.createTextNode("a new line text\n"))
        }

        removeBtn.onclick = () => {
            nodeLt.remove()
        }
    </script>
</body>
</html>

        使用 Chorme 的 DevTools-Performance做一些分析。

1、开启【Performance】项的记录
2、执行一次 CG,创建基准参考线
3、连续单击【add】按钮 3 次,增加 3 个文本节点
4、单击【remove】按钮,list列表
5、执行一次 CG
停止记录堆分析

         如上图所示,连续执行三次 add 后,节点数依次上升,但执行 remove 后,节点数却并没有下降,由此得出DOM存在泄漏,进而可导致内存泄漏。

        接下来,使用 Chorme 的 DevTools-Memory做一些分析。

1、选中【Heap snapshot】选项
2、连续单击【add】按钮 3 次,添加 3 个文本节点
3、单击开始【Take heap snapshot】按钮,执行一次堆快照
4、单击【remove】按钮,删除list列表
5、单击开始【Take heap snapshot】按钮,执行一次堆快照
6、选中生成的第二个快照报告,并将视图由"Summary"切换到"Comparison"对比模式
7、在过滤输入框中输入关键字:Detached

        从快照比较可知,导致list列表无法回收的原因是,列表节点已从DOM树中删除,但JS代码中还存在着对列表节点的引用(变量nodeLt),因为无法回收列表节点,连带着其内的文本节点也无法被回收。

        解决方案,删除DOM节点的同时,将JS代码中节点的引用释放掉。

        使用 Chorme 的 DevTools-Performance重新检测如下图。

        使用 Chorme 的 DevTools-Memory重新检测如下图。

        备注:Chorme 中Memory工具里面的Detached是什么意思?

        Detached指的是一个元素从DOM中被移除,但仍存在于内存中。这种情况下,元素不在与页面上的任何其他元素相关联。当一个元素被删除后,它就变成了Detached状态。

        通常情况下,当一个元素被删除后,因为没有任何引用指向它,它将被垃圾回收器清除。然而,如果代码中仍然保留了对该元素的引用,那么它将继续存在于内存中,并被认为是Detached状态。在某些情况下,需要在将元素从DOM中删除后继续操作它,这时可以将它保存在变量中,然后在需要时重新插入到DOM中。但是,需要注意的是,过多的Detached元素可能会导致内存泄漏,因此应该尽可能避免这种情况。

事件监听

        在组件中绑定事件时,如果在组件销毁时没有对事件进行解绑,当组件被销毁后,该事件仍然存在,可能会导致内存泄漏。因此,建议在组件销毁时解绑所有事件。

猜你喜欢

转载自blog.csdn.net/weixin_42472040/article/details/129776302
今日推荐