引言

最近有一个工单是说用户在使用我们的系统的时候,如果使用某个页面的次数多了以后浏览器就开始变慢甚至卡死崩溃掉。这个问题明显是提示有内存泄露,今天就由这个问题开始分享一些关于内存泄漏的知识。

一、 Web 应用内存泄漏的危害与易忽略性

危害:

  • 性能下降:内存泄漏导致浏览器内存占用持续增长,页面卡顿、响应延迟,最终可能崩溃
  • 资源耗尽:移动设备电池消耗加剧,低端设备体验恶化
  • 跨页面影响:SPA(单页应用)因无页面刷新,泄漏累积更严重

易忽略原因:

  • 渐进性:泄漏初期症状不明显,用户仅感知轻微卡顿,难及时反馈
  • 工具缺失:开发者缺乏实时监控手段,需主动使用 DevTools 分析
  • 框架依赖:Vue/React 等框架的自动回收机制让开发者误以为无需手动管理

二、 什么是内存泄漏

  • 核心概念:程序申请内存后未释放,导致无效内存占用持续增长
  • JavaScript 中的表现:对象未被 GC 回收。即使不再使用,仍被全局变量、闭包或事件监听器引用

常见场景:

1. 意外的全局变量

// javascript
function leak() {leakedVar = '全局泄漏'; // 未使用var/let/const,成为window属性
}
  • 原因:未声明的变量会挂载到window对象,直到页面关闭才释放。
  • 解决:严格模式(‘use strict’)或明确声明变量

2. 未清除的定时器与回调

// javascript
const intervalId = setInterval(() => {// 重复操作
}, 1000);
// 未调用 clearInterval(intervalId)
  • 影响:定时器持续引用函数,阻止垃圾回收(GC)。
  • 解决:在组件卸载或不再需要时清除定时器(clearInterval/clearTimeout)。

3. DOM引用未释放

// javascript
const elements = {button: document.getElementById('myButton')
};
document.body.removeChild(elements.button); // 移除了DOM节点
// 但 elements.button 仍保留引用
  • 原因:JavaScript对象持有DOM引用,即使节点已从DOM树移除。
  • 解决:移除节点后手动置空引用(elements.button = null)

4. 闭包引用

// javascript
function createClosure() {const largeData = new Array(1000000).fill('data');return () => console.log(largeData); // 闭包持有largeData
}
const closure = createClosure(); // largeData无法被GC回收
  • 风险:闭包可能无意中持有大型数据结构。
  • 解决:避免在闭包中保留不必要的数据,必要时手动解除引用。

5.未移除的事件监听器

// javascript
document.addEventListener('click', handleClick);
// 页面卸载时未调用 removeEventListener

  • 后果:事件监听器阻止相关对象被回收。
  • 解决:使用removeEventListener或在框架中利用生命周期钩子(如Angular的OnDestroy清理函数)

6. Web Workers未终止

// javascript
const worker = new Worker('worker.js');
// 未调用 worker.terminate()
  • 影响:Worker线程持续占用内存。
  • 解决:在不需要时调用worker.terminate()。

7. 缓存无限增长

// javascript
const cache = {};
function cacheData(key, data) {cache[key] = data; // 无缓存淘汰机制
}
  • 问题:缓存未设置上限或过期策略。
  • 解决:实现LRU(最近最少使用)等缓存淘汰策略。

三、 出发去找内存泄露

我们可以最大限度利用Chrome提供的工具来诊断内存泄露,我们一般有如下几种方式来诊断:

  • 堆快照分析: 使用Chrome DevTools的堆快照功能记录内存状态,通过比较操作前后的快照差异来定位泄露对象。 对比视图(Comparison View)可显示操作后新增或未释放的对象,帮助确认泄露。 重点关注DOM节点泄露,例如已分离的DOM子树(Detached DOM Tree)因未被垃圾回收而持续占用内存。
  • 分配分析器工具: 通过分配分析器(Allocation Profiler)实时跟踪内存分配,识别频繁创建且未释放的对象。
  • 保留路径分析: 在堆快照中检查对象的保留路径(Retainers),分析为何对象未被释放。可忽略无关保留器以简化分析。
  • 重复字符串与闭包检查: 过滤重复字符串(Duplicate Strings)和闭包(Closures),命名函数有助于区分闭包内存占用

四、开始诊断

如果你的应用要运行在移动端的浏览器中,那么对于内存的使用会更严格一些。需要在不同性能的设备上进行测试。但是我们这次主要是面对的是PC端,所以在测试环节会没有那么复杂。

1. 使用 Chrome 任务管理器实时监控内存使用情况

使用 Chrome 任务管理器作为调查内存问题的起点。Task Manager 是一个实时监视器,类似windows任务管理器,它能告诉页面使用了多少内存。

  • 按 Shift+Esc 或者从 Chrome 主菜单选择 更多工具 > 任务管理器 打开任务管理器
  • 然后右键单击 Task Manager 的表窗口启用 JavaScript 内存 。

  • 实际的效果

  • Memory footprint (内存占用) 列表示 OS 内存。DOM 节点存储在 OS 内存中。如果此值增加,则表示正在创建 DOM 节点。
  • JavaScript Memory 列表示 JS 堆。这个列包含两个值。值得注意的是实时数字(括号中的数字)。活动数字表示页面上的可访问对象使用的内存量。如果此数量增加,则表示正在创建新对象,或者现有对象正在增长。

2. 使用性能记录可视化内存泄漏

可以使用Performance(性能)面板作为另一种调查方式。Performance(性能)面板可以让我们可视化的调查内存随着时间推移的使用情况.

  • 在 DevTools 中打开 Performance (性能 ) 面板。
  • 启用 Memory 复选框。

  • 进行录制 ,最好在每次开始录制和结束前进行强制垃圾回收,点击小扫帚图标进行垃圾回收。
  • 实际效果

记录下每次的内存数据,然后强制GC再次记录。观察如果内存数据持续增加不会被GC释放,则说明可能存在内存泄漏。

3. 上述的两种办法是初步的判断办法,下面我们以诊断分离树造成的内存泄漏为例,进行进一步分析。

首先什么是分离树(Detached DOM Tree)? 在v8执行GC的时候只有当页面的 DOM 树或 JavaScript 代码中没有对 DOM 节点的引用时,才能对 DOM 节点进行垃圾回收。当一个节点从 DOM 树中删除时,该节点会成为 “detached”的状态,但如果某些 JavaScript 仍然引用它就会造成内存泄露。

分离的 DOM 节点是内存泄漏的常见原因。这里使用 DevTools 的堆分析器来识别分离的节点。

好的,我们先开始新建一个Angular的简单APP

在Page1中,添加了监听事件统计鼠标的点击次数,随着点击次数的增加,改变背景颜色。 代码如下:

page1.html

  <div><h1>This is page 1</h1><page-click-counter></page-click-counter></div>

pageClickCounter.html

<div id="page-counter-child-view" style="border-radius: 10px; padding: 5px;"><h1>This is page counter, it will show the user click count number:</h1><h2>click: {{clickCount()}}</h2>
</div>

page1.ts

  import { Component } from '@angular/core';import { PageClickCounter } from '../pageClickCounter/pageClickCounter';@Component({selector: 'app-page1',imports: [PageClickCounter],templateUrl: './page1.html',styleUrl: './page1.less'})export class Page1 {}

pageClickCounter.ts

  import { Component, signal, AfterViewInit, OnDestroy } from '@angular/core';@Component({selector: 'page-click-counter',imports: [],templateUrl: './pageClickCounter.html',styleUrl: './pageClickCounter.less'})export class PageClickCounter implements AfterViewInit {protected clickCount = signal(0);childView: HTMLElement | null = null;ngAfterViewInit(): void {this.childView = document.querySelector('#page-counter-child-view');document.addEventListener('click', this.clickHandler);}clickHandler = () => {this.clickCount.update(count => count + 1);console.log('Page1 click count:', this.clickCount());// Update background color based on click count// Use HSL color with hue changing from green (120) to red (0) as clicks increaseconst hue = Math.min(120 - (this.clickCount() * 5), 120);(this.childView as HTMLElement).style.backgroundColor = `hsl(${hue}, 70%, 60%)`;};}

实际的运行效果如下:

这个Demo里已经存在泄露了,这里我们使用DevTools的堆分析器进行内存泄漏的检测。

  • 点击录制,录制好的快照如下:

  • 在搜索框输入 detached 搜索分离的DOM树节点:

看这个搜索结果的表,里面有四个列:

  • Constructor: 表示分离的DOM节点的构造类型
  • Distance: 节点与根节点的距离

在浏览器中GC回收的根节点就是window对象,其他的对象或者基本类型都是从这里出发链接到一起的。

  • Shallow size:这是对象本身持有的内存大小。典型的 JavaScript 对象会保留一些内存用于其描述和存储即时值。通常,只有数组和字符串可以具有明显的浅层大小。
  • Retained size:这是对象及其所有子对象所占的内存大小。更精确的描述是删除对象本身及其无法从 GC 根访问的依赖对象后释放的内存大小。比如上图中的节点6和8,节点8依赖节点6儿存在,如果节点6被删除,那么节点8就无法访问。

现在我们了解了快照表上的几个列的含义,点击一个行在下面的Retainer表和看到详细的引用情况。然后我们就可以找到泄露产生的位置,来用对应的办法解决。

但是看我们搜索出来的结果很杂乱,而且在实际的复杂项目中这个结果可能更加的复杂。那我们要从哪里开始下手呢?

其实在我们Angular或者Vue这些一组件为基础组装的应用中,如果我们从 <div> 或者 <h1> 这些节点开始向上找的话大概率会找到一个自定义的组件为止。

所以这里开始解决的小技巧是从大的组件开始解决,因为好多搜索出来的基础元素泄露可能只是被自定义组件持有,当我们解决了组件级别的泄露,这些小的元素泄露会跟着消失。

好,看回我们的Demo在列表里发现了我们的自定义组件 page-click-counter,点击进去

这里提示我们的 clickHandler 函数的引用关系,我们点击进去

我们分析代码,这里的childView持有了页面上的元素,然后订阅了document的click事件。问题出现在页面销毁的时候这个点击事件的定义还在,clickHandler函数持有的DOM 对象childView就成为了分离的DOM。

好,我们开始解决这个问题。在页面销毁的生命周期函数里把订阅取消。

// javascript
ngOnDestroy(): void {// Clean up the event listener to prevent memory leaksdocument.removeEventListener('click', this.clickHandler);
}

重新编译运行,然后同样的方法记录内存快照。

我们可以看到,内存快照中不再有刚才的泄露对象。

五、 其他内存检测方案

浏览器内置工具:

  • Chrome DevTools:
    • Heap Snapshot:对比多次快照,识别未释放对象

  • Allocation Timeline:跟踪内存分配时间线,定位泄漏点

六、 预防策略与未来方向

代码规范:

  • 及时释放资源:
    • 事件监听器、定时器在 ngOnDestroy 中移除
// javascript
ngOnDestroy(): void {this.subscription.unsubscribe(); // 清理RxJS订阅document.removeEventListener('click', this.handler);
}
  • 使用 WeakMap 替代强引用存储临时数据
// javascript    
// 使用WeakMap存储临时数据   
const weakMap = new WeakMap();   
const element = document.getElementById('target');   weakMap.set(element, { metadata: 'data' });   
// 当element被移除时,关联数据可被GC回收
  • 避免全局变量:严格模式(use strict)禁用意外全局声明

架构优化:

  • 资源隔离:为组件分配独立作用域,避免交叉引用
// javascript
// Angular服务作用域隔离示例
@Injectable({ providedIn: 'root' }) // 根作用域
class RootService {}@Injectable({ providedIn: 'platform' }) // 平台级作用域
class PlatformService {}@Injectable({ providedIn: 'any' }) // 每个模块独立实例
class ModuleService {}
  • 团队实践:定期代码审查,重点关注事件绑定、闭包和第三方库

审查项

检查点

事件管理

是否在组件销毁时移除监听器

订阅管理

RxJS订阅是否正确取消

DOM引用

是否存在未释放的DOM引用

缓存机制

是否设置过期策略

未来趋势 现在我们可以借助AI大模型和MCP来实现文件扫描,让大模型可以扫描并分析代码查找可能出现内存泄露的写法,并给出修改建议。

以下就是AI扫描pageClickCounter.ts文件后给出的修复建议。

The following memory leak issues were identified in the pageClickCounter.ts file:

1. Missing Event Listener Cleanup
The clickHandler event listener attached to document in ngAfterViewInit is not removed in ngOnDestroy.

This creates a reference cycle:

  • document retains a reference to clickHandler
  • clickHandler retains a reference to the component instance via this
  • Prevents Angular’s garbage collection of the component

2. Unmanaged DOM Reference
The childView property retains a direct DOM element reference through: javascript  

// javascript   this.childView = document.querySelector('#page-counter-child-view'); 

While less critical than the event listener issue, this could prolong DOM element lifecycle beyond component destruction if not explicitly nullified.

Recommended Fix:
Uncomment and implement ngOnDestroy to clean up resources:

// javascriptngOnDestroy(): void {// 1. Remove event listenerdocument.removeEventListener('click', this.clickHandler);// 2. Clear DOM referencethis.childView = null;}

Analysis Context:
- This is an Angular component using reactive signals (signal()) - The memory leak occurs through:
document -> clickHandler -> component -> childView -> DOM Element - The event handler pattern follows Angular’s best practices but requires explicit cleanup for document/window listeners

七、结论

与应用程序中的内存泄漏作斗争可能是一项艰巨的任务。但是通过执行以上这些步骤,我们可以了解内存泄漏模式确定内存泄漏的根本原因,并实施必要的清理以防止进一步危害。而且现在我们现在有了LLM工具,我们可以使用大模型来分析代码并确定内存泄漏的根本原因,并使这个自动化过程更加高效和有效。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/86344.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/86344.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/86344.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在云服务器上搭建 MinIO 图片存储服务器及 Spring Boot 整合实现图片上传下载

一、MinIO 核心概念 MinIO 是一个高性能的分布式对象存储服务器&#xff0c;兼容 Amazon S3 API&#xff0c;具有以下特点&#xff1a; 高性能&#xff1a;针对存储和检索优化 轻量级&#xff1a;单个二进制文件即可运行 云原生&#xff1a;支持 Kubernetes 部署 S3 兼容&a…

《深入解析:如何通过CSS集成WebGPU实现高级图形效果》

当CSS的细腻笔触遇上WebGPU的磅礴算力&#xff0c;两者如同命运交织的织工&#xff0c;以代码为丝线&#xff0c;在虚拟空间中编织出超越现实维度的灵境。这场融合不再局限于视觉呈现的革新&#xff0c;而是创造出一种能够与用户情感共鸣、突破物理法则束缚的沉浸式数字体验&am…

R 语言科研绘图 --- 环状图-汇总

在发表科研论文的过程中&#xff0c;科研绘图是必不可少的&#xff0c;一张好看的图形会是文章很大的加分项。 为了便于使用&#xff0c;本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中&#xff0c;获取方式&#xff1a; R 语言科研绘图模板 --- sciRplothttps://mp.…

突破限制:实现页面内精准监听 localStorage 变更

突破限制&#xff1a;实现页面内精准监听 localStorage 变更 一、简介二、示例演示三、StorageEvent重构setItem四、CustomEvent自定义事件同一页面不同模块数据同步五、MessageChannel同一页面不同模块数据同步六、BroadcastChannel多窗口数据同步七、CustomEventBroadcastCha…

牛客AI面试破解电销招聘效率与成本双重难题

在电销行业&#xff0c;高流动性与大规模招聘需求长期困扰企业人力资源管理。传统招聘模式下&#xff0c;HR需应对海量简历筛选、多轮面试协调、主观评估偏差等挑战&#xff0c;导致招聘周期长、成本高、人才匹配度低。如何通过技术手段实现精准筛选与效率提升&#xff1f;牛客…

智慧生产管控数字化平台(源码+文档+讲解+演示)

引言 在全球化和信息化的浪潮中&#xff0c;制造业正面临着前所未有的挑战和机遇。智慧生产管控数字化平台应运而生&#xff0c;旨在通过数字化手段优化生产管控的全流程。本文将详细介绍智慧生产管控数字化平台的核心功能、技术架构以及如何通过开源代码实现二次开发&#xf…

用Tensorflow进行线性回归和逻辑回归(九)

用TensorFlow训练线性和逻辑回归模型 这一节结合前面介绍的所有TensorFlow概念来训练线性和逻辑回归模型&#xff0c;使用玩具数据集。 用TensorFlow训练模型 假如我们指明了数据点和标签的容器&#xff0c;定义了张量操作的损失函数。添加了优化器节点到计算图&#xff0c;…

使用 vue vxe-table 实现复选框禁用,根据行规则来禁用是否允许被勾选选中

使用 vue vxe-table 实现复选框禁用&#xff0c;根据行规则来禁用是否允许被勾选选中 查看官网&#xff1a;https://vxetable.cn 禁用选中 通过 checkMethod 方法控制 checkbox 是否允许用户手动勾选&#xff0c;如果被禁用&#xff0c;可以调用 setCheckboxRow 方法手动设置…

【Linux-网络】深入拆解TCP核心机制与UDP的无状态设计

&#x1f3ac; 个人主页&#xff1a;谁在夜里看海. &#x1f4d6; 个人专栏&#xff1a;《C系列》《Linux系列》《算法系列》 ⛰️ 道阻且长&#xff0c;行则将至 目录 &#x1f4da;引言 &#x1f4da;一、UDP协议 &#x1f4d6; 1.概述 &#x1f4d6; 2.特点 &#x1…

(nice!!!)(LeetCode 每日一题) 2081. k 镜像数字的和 (枚举)

题目&#xff1a;2081. k 镜像数字的和 思路&#xff1a;枚举10进制的回文串&#xff0c;然后来判断对应的k进制数是否是回文串。直到有n个满意即可。 而枚举10进制的回文串&#xff0c;从基数p(1、10、100… )开始&#xff0c;长度为奇数的回文串&#xff0c;长度为偶数的回文…

Java面试题027:一文深入了解数据库Redis(3)

Java面试题025&#xff1a;一文深入了解数据库Redis&#xff08;1&#xff09; Java面试题026&#xff1a;一文深入了解数据库Redis&#xff08;2&#xff09; 本节我们整理一下Redis高可用和消息队列使用场景的重点原理&#xff0c;让大家在面试或者实际工作中遇到这类问题时…

算法打卡 day4

4 . 高精度算法 性质&#xff1a;数组或者容器从低位往高位依次存储大整数&#xff0c;方便进位。 4.1 高精度加法 给定两个正整数&#xff08;不含前导 0&#xff09;&#xff0c;计算它们的和。 输入格式 共两行&#xff0c;每行包含一个整数。 输出格式 共一行&#xff0c;…

【笔记】Docker 配置阿里云镜像加速(公共地址即开即用,无需手动创建实例)

2025年06月25日记 【好用但慎用】Windows 系统中将所有 WSL 发行版从 C 盘迁移到 非系统 盘的完整笔记&#xff08;附 异常处理&#xff09;-CSDN博客 【笔记】解决 WSL 迁移后 Docker 出现 “starting services: initializing Docker API Proxy: setting up docker ap” 问题…

day35-Django(1)

day35-Django 3.2 前言 之前我们介绍过web应用程序和http协议,简单了解过web开发的概念。Web应用程序的本质 接收并解析HTTP请求,获取具体的请求信息处理本次HTTP请求,即完成本次请求的业务逻辑处理构造并返回处理结果——HTTP响应import socketserver = socket.socket() …

PostgreSQL全栈部署指南:从零构建企业级高可用数据库集群

PostgreSQL全栈部署指南:从零构建企业级数据库集群 前言: 本文详解了**PostgreSQL**所有的部署方式,如 yum 安装、源码编译安装、RPM包手动安装,以及如何选择适合的安装方式。适合不同的场景应用。通过高可用部署详细了解安装思路及过程,包括内网环境下的配置、主节点的创…

MQTT 和 HTTP 有什么本质区别?

MQTT 和 HTTP 的本质区别在于它们设计的初衷和核心工作模式完全不同。它们是为解决不同问题而创造的两种工具。 简单来说&#xff1a; HTTP 就像是去图书馆问问题&#xff1a;你&#xff08;客户端&#xff09;主动去找图书管理员&#xff08;服务器&#xff09;&#xff0c;…

GtkSharp跨平台WinForm实现

文章目录 跨平台架构设计跨平台项目配置GtkSharp串口通讯实现跨平台部署配置Linux系统配置macOS系统配置 相关学习资源GTK#跨平台开发跨平台.NET开发Linux开发环境macOS开发环境跨平台UI框架对比容器化部署开源项目参考性能优化与调试 跨平台架构设计 基于GTKSystem.Windows.F…

【闲谈】对于c++未来的看法

对于C未来看法 C 作为一门诞生于上世纪的编程语言&#xff0c;在软件工业发展史上扮演了不可替代的角色。尽管近年来诸如 Rust、Go、Swift、Kotlin 等现代语言相继崛起&#xff0c;C 依然在系统软件、高性能服务、嵌入式等关键领域中发挥着主力作用。本文将从 C 的当前应用前景…

【论文】云原生事件驱动架构在智能风控系统中的实践与思考

摘要 2023年6月至2024年3月,我作为某头部证券公司新一代极速交易系统的首席架构师,主导设计并落地了基于云原生事件驱动架构的全新交易风控平台。该项目旨在攻克原有系统无法支撑峰值20万笔/秒交易量、风控延迟超过3秒以及行情剧烈波动时系统崩溃等核心痛点。通过构建以Kube…

opensbi从0到1入门学习

最近要在RV64的平台上把Linux给bringup起来&#xff0c;由于当下的工作主要集中在底层硬件接口驱动、CPU的操作及RTOS应用等&#xff0c;虽然之前搞过Arm Linux的开发工作&#xff0c;但是比较基础的玩的比较少&#xff0c;所以真正要搞把系统bringup起来&#xff0c;我之前的知…