云计算百科
云计算领域专业知识百科平台

内存泄漏排查实战:从定位到修复

说实话,前端开发里最让人头皮发麻的问题,内存溢出(Memory Leak)绝对排前三。因为它不像报错那样会直接给你一个红叉和堆栈信息,而是悄无声息地让你的页面越用越卡,最后直接崩掉。

我记得有一次,公司的一个后台管理系统,用户反馈说“用了一上午就卡得点不动了”。我们一开始还以为是接口慢,查了半天发现接口响应都正常。后来打开任务管理器一看,好家伙,标签页内存占用快2个G了。这就是典型的内存泄漏。

今天咱们就来聊聊,当页面出现内存问题时,应该怎么一步步去排查、定位、修复。不讲虚的,直接上实操。

1. 先判断:真的是内存溢出吗?

有时候页面卡顿,不一定是内存问题,可能是CPU占用高、网络慢、或者DOM节点太多导致的渲染卡顿。所以第一步,得确认是不是内存泄漏。

最直观的方法就是打开浏览器的任务管理器(Chrome里按 Shift + Esc),找到你的标签页,观察两个指标:

  • 内存占用:如果这个数值一直在增长,没有回落,说明有内存泄漏

  • CPU占用:如果CPU一直居高不下,可能是密集计算或者频繁重绘

另一个方法是打开开发者工具(F12) -> Performance(性能) 面板,录制一段操作,然后看内存曲线(Memory)。正常的内存曲线应该是有升有降的(因为垃圾回收会定时清理),如果曲线是一路向上不带回头的,那基本可以确定有内存泄漏。

2. 抓“元凶”:Heap Snapshot 快照对比法

确认有内存泄漏之后,接下来要找出是哪个对象一直在占用内存。这时候要用到 Memory(内存) 面板里的 Heap Snapshot(堆快照)。

操作步骤:

第一步:打开页面,做一次初始操作(比如刚加载完),拍第一个快照(Snapshot 1)。

第二步:重复执行某个你认为可能有问题的操作(比如打开一个弹窗再关闭,反复10次)。

第三步:拍第二个快照(Snapshot 2),然后切换到 Comparison(对比) 视图,选择跟 Snapshot 1 对比。

这时候你会看到一个列表,里面列出了“新增的对象”和“仍然存活的对象”。重点关注那些 #New 数量多、且没有被释放的对象。

比如你发现有个 Window 对象或 EventEmitter 实例一直没被释放,那很可能就是事件监听没移除。如果你发现一堆 HTMLDivElement 或 HTMLSpanElement 没被释放,那就是DOM节点没有被正确回收。

3. 实战案例一:定时器引发的血案

这是最经典的内存泄漏场景之一。

看这段代码:

export default {
data() {
return {
count: 0
};
},
mounted() {
this.timer = setInterval(() => {
this.count++;
console.log(this.count);
}, 1000);
},
// 忘记在 beforeUnmount 里清除定时器
};

每次进入这个页面,都会创建一个新的定时器。但离开页面时,定时器没有被清除,它仍然持有对组件实例 this 的引用。组件实例无法被垃圾回收,里面的数据、DOM引用全被“锁”住了。

页面进出个十几次,内存里就攒了十几个“僵尸组件”。

排查方法:在 Heap Snapshot 里搜索 setInterval 或者你的组件名,看看是不是有多个实例同时存在。

修复方法:

beforeUnmount() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}

如果用的是 Vue3 的 setup 语法,就放在 onUnmounted 里。

4. 实战案例二:事件监听没有移除

事件监听也是重灾区。特别是在 mounted 里绑定了全局事件,然后在 beforeUnmount 里忘了移除。

mounted() {
window.addEventListener('resize', this.handleResize);
document.addEventListener('scroll', this.handleScroll);
},
// 忘了在 beforeUnmount 里 removeEventListener

当组件销毁后,这些事件监听依然挂在 window 或 document 上,而且它们的回调函数 handleResize 绑定了组件的 this,导致整个组件实例无法被回收。

排查方法:在 Heap Snapshot 里搜索 handleResize 或 EventListener,看是否有被卸载组件的事件监听仍然存在。

修复方法:成对出现,成对消失。

beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('scroll', this.handleScroll);
}

还有一个容易忽略的点:用 addEventListener 绑定了 DOM 事件的匿名函数,是无法移除的,所以一定要用具名函数。

5. 实战案例三:闭包导致的大对象无法释放

闭包是个好东西,但用不好也会造成内存泄漏。

function createHeavyObject() {
const bigData = new Array(1000000).fill('*'); // 大数组
return function() {
// 这个函数虽然没有直接使用 bigData
// 但因为闭包的作用,bigData 一直活在内存里
console.log('hello');
};
}
const fn = createHeavyObject();
// fn 一直存在,bigData 就永远释放不了

更隐蔽的版本是 Vue 的 computed 或 watch 里使用了外部的大对象:

const bigData = ref(new Array(1000000).fill('*'));

const someComputed = computed(() => {
// 这里如果返回了 bigData 的一部分,并且被长期引用
return bigData.value.slice(0, 100);
});
// 如果不小心把 someComputed 赋值给了一个全局变量,那整个 bigData 都释放不了

排查方法:在 Heap Snapshot 里按“Retained Size(保留大小)”排序,看哪些对象占用的内存最大,然后顺着引用链往上找,看看是谁在引用它。

修复方法:确保不需要的时候,把引用置为 null。或者在闭包中只保留必要的数据,不要“连带”一大堆无关的东西。

6. 实战案例四:DOM 节点引用未清理

当你用变量缓存了一个DOM节点,但该节点被从页面上移除了,你的变量仍然指向它,导致这个DOM节点无法被垃圾回收。

let cachedDiv = document.getElementById('cached');

function removeDiv() {
const div = document.getElementById('cached');
div.parentNode.removeChild(div);
// 但 cachedDiv 变量还指向这个被移除的 div
// 这个 DOM 节点就永远停留在内存里了
}

排查方法:在 Heap Snapshot 里搜索 Detached(分离的)关键字,可以找到所有已经从DOM树中移除但还被JS引用的节点。

修复方法:移除DOM节点时,顺手把JS引用也置空。

function removeDiv() {
const div = document.getElementById('cached');
div.parentNode.removeChild(div);
cachedDiv = null; // 手动释放
}

7. Chrome DevTools 的几个实用小技巧

① 录制性能并查看内存曲线

在 Performance 面板录制一段操作,勾选 Memory 选项。录制结束后,看底部的 Heap 曲线,如果曲线在垃圾回收(图中会有小三角标记)后没有明显下降,说明有内存泄漏。

② 使用 Allocation Timeline(分配时间线)

在 Memory 面板选择 Allocation instrumentation on timeline,然后开始录制,它会实时显示内存分配的情况。如果某个操作后,内存分配量突然暴增且没有回落,那就是问题点。

③ 手动触发垃圾回收

在 Memory 面板左侧有个小垃圾桶图标,点击可以手动触发垃圾回收。在拍快照前点一下,可以排除一些“暂存”对象的干扰,让快照更干净。

8. 预防大于排查

与其等到内存溢出再排查,不如在编码时就养成好习惯:

场景正确做法
定时器 / 间隔器 mounted 里创建,beforeUnmount 里清除
事件监听(全局) 绑定和解绑成对出现
第三方库订阅 调用 subscribe 后,记得存下 unsubscribe 并调用
大数组 / 大对象 用完后手动置 null 释放引用
DOM 节点引用 节点移除后,变量也置 null
请求(fetch/axios) 用 AbortController 在组件销毁时取消未完成的请求
总结一下排查流程
  • 观察现象:页面越来越卡,内存占用持续攀升

  • 确认问题:用 Chrome 任务管理器或 Performance 面板确认内存泄漏

  • 定位对象:用 Heap Snapshot 对比法,找到泄漏的对象类型

  • 追溯代码:查看对象引用链,找到是谁在持有它

  • 修复根因:按上面表格里的方式,清理定时器、监听、引用等

  • 验证修复:重新跑一遍操作,确认内存曲线恢复正常

  • 排查内存泄漏确实是个细活,有时候一个疏忽就能让你折腾一下午。但一旦掌握了这套方法,大部分内存问题都能迎刃而解。

    如果你在实际项目中遇到过更奇葩的内存泄漏案例,欢迎在评论区分享出来,大家一起涨姿势。觉得有用的话,点个收藏,下次排查的时候直接翻出来对照着来~cPpzQ.cRv.ltD
    dYYxs.cRv.ltD
    bI7dR.cRv.ltD
    ScY3Y.cRv.ltD
    x0ewr.cRv.ltD
    qu7gn.cRv.ltD
    v4979.cRv.ltD
    p9u7j.cRv.ltD
    CchvN.cRv.ltD
    M9peu.cRv.ltD
    qOAXs.cRv.ltD
    tbzSw.cRv.ltD
    Aa1EC.cRv.ltD
    zPEc9.cRv.ltD
    upHlA.cRv.ltD
    gyZA4.cRv.ltD
    zVQTq.cRv.ltD
    kZuIc.cRv.ltD
    t8t66.cRv.ltD
    amW56.cRv.ltD
    Ovn39.cRv.ltD
    0DOCw.cRv.ltD
    G0Tex.cRv.ltD
    qqES6.cRv.ltD
    kQ1g4.cRv.ltD
    mx4ww.cRv.ltD
    Fm1zP.cRv.ltD
    urhYf.cRv.ltD
    aO33R.cRv.ltD
    ULocv.cRv.ltD
    95SmK.cRv.ltD
    EpRv4.cRv.ltD
    N1Mwr.cRv.ltD
    FMmPV.cRv.ltD
    SUjMY.cRv.ltD
    5rMtc.cRv.ltD
    R7043.cRv.ltD
    jwz5f.cRv.ltD
    fyPPm.cRv.ltD
    JjIe9.cRv.ltD
    qF4En.cRv.ltD
    gaynI.cRv.ltD
    qMbZM.cRv.ltD
    4OOYN.cRv.ltD
    KuOFx.cRv.ltD
    vT71A.cRv.ltD
    0ty5p.cRv.ltD
    PMMwp.cRv.ltD
    V2cTK.cRv.ltD
    JENZQ.cRv.ltD
    hBrjn.cRv.ltD
    bSiPU.cRv.ltD
    UrmmN.cRv.ltD
    oQJaY.cRv.ltD
    uErPI.cRv.ltD
    vAt8Z.cRv.ltD
    y8xll.cRv.ltD
    3osMl.cRv.ltD
    SYXN7.cRv.ltD
    smRSQ.cRv.ltD
    TyQzZ.cRv.ltD
    sLs3l.cRv.ltD
    JPsBx.cRv.ltD
    GoQh7.cRv.ltD
    FfXDh.cRv.ltD
    zr8ib.cRv.ltD
    UlbA7.cRv.ltD
    3INxM.cRv.ltD
    VQTMC.cRv.ltD
    aMRZm.cRv.ltD
    0w7VB.cRv.ltD
    35R0Q.cRv.ltD
    eEGkZ.cRv.ltD
    O411c.cRv.ltD
    2IY8g.cRv.ltD
    BLqkD.cRv.ltD
    dSEdS.cRv.ltD
    dMdsa.cRv.ltD
    6Fcis.cRv.ltD
    nRuzT.cRv.ltD
    NJASf.cRv.ltD
    2r5iO.cRv.ltD
    H9ZpJ.cRv.ltD
    K33UV.cRv.ltD
    0gqA7.cRv.ltD
    YKA6E.cRv.ltD
    d1l8A.cRv.ltD
    vaQyC.cRv.ltD
    1MF9z.cRv.ltD
    HIATz.cRv.ltD
    ikPUP.cRv.ltD
    4b9g4.cRv.ltD
    1O4Xi.cRv.ltD
    xHHcO.cRv.ltD
    8MNrN.cRv.ltD
    jNz7r.cRv.ltD
    5kVSq.cRv.ltD
    rj5eC.cRv.ltD
    uqykx.cRv.ltD
    s3yUU.cRv.ltD
    SBCrm.cRv.ltD
    7tib9.cRv.ltD
    10Oqt.cRv.ltD
    bTjad.cRv.ltD
    kGSB3.cRv.ltD
    FT6DW.cRv.ltD
    4OmZJ.cRv.ltD
    KgvTe.cRv.ltD
    atFUR.cRv.ltD
    byh8F.cRv.ltD
    4OKs0.cRv.ltD
    ACsle.cRv.ltD
    9lNB7.cRv.ltD
    EckIv.cRv.ltD
    Hytxg.cRv.ltD
    ZmBI3.cRv.ltD
    jlwAt.cRv.ltD
    mazvE.cRv.ltD
    zqltz.cRv.ltD
    r9uRe.cRv.ltD
    2kbhp.cRv.ltD
    iwlBq.cRv.ltD
    sxCXG.cRv.ltD
    g98QO.cRv.ltD
    Ml2FJ.cRv.ltD
    j1abI.cRv.ltD
    hAXE8.cRv.ltD
    J6v6C.cRv.ltD
    XTmUw.cRv.ltD
    ZvD7O.cRv.ltD
    nrP5J.cRv.ltD
    Z7aWX.cRv.ltD
    qkWce.cRv.ltD
    Q1OM1.cRv.ltD
    8o9Is.cRv.ltD
    Cqksl.cRv.ltD
    ZQc8g.cRv.ltD
    GjeyS.cRv.ltD
    amyJ5.cRv.ltD
    nXs19.cRv.ltD
    MkH0w.cRv.ltD
    mdlS6.cRv.ltD
    3p4F5.cRv.ltD
    LwFsS.cRv.ltD
    mBMmF.cRv.ltD
    17ZZ7.cRv.ltD
    JKpLy.cRv.ltD
    su3Sq.cRv.ltD
    mnBzl.cRv.ltD
    RP9Sj.cRv.ltD
    iXTfA.cRv.ltD
    NSjbm.cRv.ltD
    OO3Jz.cRv.ltD
    tv0B9.cRv.ltD
    f8ihc.cRv.ltD
    oTTGr.cRv.ltD
    S18wf.cRv.ltD
    GlInj.cRv.ltD
    W8aaY.cRv.ltD
    eQzqO.cRv.ltD
    uH5KD.cRv.ltD
    Fnv0q.cRv.ltD
    qMlM6.cRv.ltD
    5tiWb.cRv.ltD
    aB05d.cRv.ltD
    u12wv.cRv.ltD
    PLkOD.cRv.ltD
    b0pkC.cRv.ltD
    EY1r4.cRv.ltD
    GVVqd.cRv.ltD
    op8NU.cRv.ltD
    yVQGB.cRv.ltD
    wXSWG.cRv.ltD
    Zl8MY.cRv.ltD
    sod2a.cRv.ltD
    DsjJ4.cRv.ltD
    glOmc.cRv.ltD
    8PTyw.cRv.ltD
    VqvLP.cRv.ltD
    Wb6bU.cRv.ltD
    uRcxC.cRv.ltD
    YfZZG.cRv.ltD
    vYwBw.cRv.ltD
    jyNRK.cRv.ltD
    KS6gy.cRv.ltD
    966tJ.cRv.ltD
    HQQJV.cRv.ltD
    vJM4M.cRv.ltD
    iUaG1.cRv.ltD
    OsmHJ.cRv.ltD
    vq3lm.cRv.ltD
    RbE3a.cRv.ltD
    wlZBv.cRv.ltD
    IKGDZ.cRv.ltD
    4EJqj.cRv.ltD
    90WJ3.cRv.ltD
    7PB0W.cRv.ltD
    jaJ88.cRv.ltD
    htuFF.cRv.ltD
    lPCll.cRv.ltD
    zEprM.cRv.ltD
    sVhkP.cRv.ltD
    osvQg.cRv.ltD
    s3iDL.cRv.ltD
    rTJBm.cRv.ltD
    BQhim.cRv.ltD
    gFZrN.cRv.ltD
    UKIfd.cRv.ltD
    ml8DJ.cRv.ltD
    HX779.cRv.ltD
    RAau2.cRv.ltD
    TE7E0.cRv.ltD
    OPtzm.cRv.ltD
    sqd3k.cRv.ltD
    ftdFy.cRv.ltD
    DzmVj.cRv.ltD
    X5ASh.cRv.ltD
    JSEcw.cRv.ltD
    xSvEk.cRv.ltD
    dRBDk.cRv.ltD
    1tshS.cRv.ltD
    V66Vt.cRv.ltD
    2SKWC.cRv.ltD
    6J8x7.cRv.ltD
    suosa.cRv.ltD
    uLtCx.cRv.ltD
    WvyDV.cRv.ltD
    NHu5l.cRv.ltD
    DAvPm.cRv.ltD
    7CW3Q.cRv.ltD
    VF3kQ.cRv.ltD
    Ai0QJ.cRv.ltD
    EjDCr.cRv.ltD
    dB29l.cRv.ltD
    O9XBA.cRv.ltD
    vIXdQ.cRv.ltD
    vh9jm.cRv.ltD
    RR9yD.cRv.ltD
    tewY5.cRv.ltD
    McR5P.cRv.ltD
    u3oOu.cRv.ltD
    jIxC6.cRv.ltD
    JHE3B.cRv.ltD
    aTCG5.cRv.ltD
    Ny1uK.cRv.ltD
    dkt0m.cRv.ltD
    AJtWW.cRv.ltD
    8SJQQ.cRv.ltD
    O3fl8.cRv.ltD
    5jv9M.cRv.ltD
    udCq4.cRv.ltD
    pjB5G.cRv.ltD
    VeHv4.cRv.ltD
    eFrpU.cRv.ltD
    qhtng.cRv.ltD
    cMIOw.cRv.ltD
    EK7mU.cRv.ltD
    PUOIe.cRv.ltD
    MiaZd.cRv.ltD
    ZlImW.cRv.ltD
    e0044.cRv.ltD
    y0teT.cRv.ltD
    g02hL.cRv.ltD
    FOqq4.cRv.ltD
    lE2Wg.cRv.ltD
    DKWT9.cRv.ltD
    yIdSO.cRv.ltD
    ubJoJ.cRv.ltD
    PZUCR.cRv.ltD
    gWMV9.cRv.ltD
    9AqkV.cRv.ltD
    mGXiS.cRv.ltD
    zLMa3.cRv.ltD
    lLpaA.cRv.ltD
    E66gd.cRv.ltD
    oxv1Q.cRv.ltD
    y1XGI.cRv.ltD
    QVuB6.cRv.ltD
    IZW1Q.cRv.ltD
    LMXA8.cRv.ltD
    gcIxM.cRv.ltD
    KTmst.cRv.ltD
    MG30N.cRv.ltD
    QPyWz.cRv.ltD
    1LhdH.cRv.ltD
    RTDsn.cRv.ltD
    XqR4Y.cRv.ltD
    FGhHx.cRv.ltD
    LAHN1.cRv.ltD
    8IpGE.cRv.ltD
    dqTzK.cRv.ltD
    o4AmR.cRv.ltD
    3tuVF.cRv.ltD
    AjfWH.cRv.ltD
    qr3D9.cRv.ltD
    jPRQc.cRv.ltD
    PoMus.cRv.ltD
    gZTgF.cRv.ltD
    ZzwX3.cRv.ltD
    gPpia.cRv.ltD
    xtmr9.cRv.ltD
    2G0a5.cRv.ltD
    AkVXC.cRv.ltD
    VtYVx.cRv.ltD
    Lr7kF.cRv.ltD
    BkBc7.cRv.ltD
    37Rad.cRv.ltD
    tUaDe.cRv.ltD
    DSbla.cRv.ltD
    SKOxI.cRv.ltD
    5vx91.cRv.ltD
    31qBh.cRv.ltD
    sajUr.cRv.ltD
    fdszI.cRv.ltD
    iyfSd.cRv.ltD
    ugKCG.cRv.ltD
    aSbNR.cRv.ltD
    uaVQc.cRv.ltD
    jVomR.cRv.ltD
    Tyqzb.cRv.ltD
    8Aata.cRv.ltD
    lxqIj.cRv.ltD
    zmo1w.cRv.ltD
    sa7F9.cRv.ltD
    TbZ5s.cRv.ltD
    efz9K.cRv.ltD
    APOGG.cRv.ltD
    92gAP.cRv.ltD
    nsqhK.cRv.ltD
    G3e79.cRv.ltD
    G8XJc.cRv.ltD
    Q4Gzi.cRv.ltD
    Rjlhu.cRv.ltD
    66uBF.cRv.ltD
    R79Bf.cRv.ltD
    6572p.cRv.ltD
    GNZyU.cRv.ltD
    oQVB1.cRv.ltD
    MwalU.cRv.ltD
    Cv9kl.cRv.ltD
    5yXj5.cRv.ltD
    wS5G8.cRv.ltD
    PRRYB.cRv.ltD
    ZZE6S.cRv.ltD
    mlsxK.cRv.ltD
    cRAH2.cRv.ltD
    fTpcJ.cRv.ltD
    IH2jh.cRv.ltD
    wOgB1.cRv.ltD
    0Wypj.cRv.ltD
    krNki.cRv.ltD
    YFvgx.cRv.ltD
    XxEXE.cRv.ltD
    24UL3.cRv.ltD
    3GPeh.cRv.ltD
    cci4z.cRv.ltD
    HKXS3.cRv.ltD
    TuT5Z.cRv.ltD
    wOLp7.cRv.ltD
    M2Li6.cRv.ltD
    kbZl4.cRv.ltD
    CUOQI.cRv.ltD
    R9Tpe.cRv.ltD
    TfIHJ.cRv.ltD
    x6Csv.cRv.ltD
    vrirp.cRv.ltD
    hfDpN.cRv.ltD
    526Dv.cRv.ltD
    pNhRM.cRv.ltD
    cZcmY.cRv.ltD
    IUufU.cRv.ltD

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 内存泄漏排查实战:从定位到修复
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!