web应用内存调优

写在开头

内存调优包括很多方面,而标题所指得内存调优是指得优化内存泄漏的问题。所谓的内存泄漏在大部分人的认知中是指的应用不需要的内存,没有得到释放。但是这个概念是不准确的,但是我们可以这样去理解。

真正的内存泄漏其实是要工程师通过源码去分析。怎么去理解这一句话。

个人理解其实是指的有些我们所认为的无用的内存没有释放其实是因为很多原因,可能并不是内存泄漏,而这些内存没有释放是不是我们的代码逻辑导致的,其实还是需要开发者通过源码去分析得出结果的。

对于内存性能优化的看法

作者最近得到反馈说应用卡顿,所以去看了内存性能优化的一些资料,当然应用卡顿其实有很多原因,内存占用问题只是其中可能的一点。还包括功能设计等等问题。

而为什么作者需要去看内存性能优化这一块呢?

其实是因为前文所所提到的用户反馈浏览器对应web应用进程内存占用太高了。

对于内存性能优化的看法:

内存性能优化的收益其实对于很多项目来说收益不高,也不是很有必要,要去解决内存泄漏问题很多时候还要去涉及改业务逻辑,所以很多时候是很麻烦的事情,涉及到很多。所以不是万不得已是不会去做。其实很多其他的优化收益更高。

那么怎么去预防随着应用越来越大内存泄漏问题越来越严重?

这只能从每个阶段去解决,而不能积累到一定程度,积累到了一定程度,就是一座座代码山了,那时候便是寸步难行。

为什么要优化内存泄漏

每个应用执行的时候必须由操作系统分配内存才能执行。对于浏览器来说也是一样,例如js执行的时候也是会由v8虚拟机去向操作系统申请分配内存。而操作系统能分配给浏览器是有限的。

那么当web应用内存占有太高的时候,那么应用执行的时候就会卡顿甚至崩溃。

v8垃圾回收机制

有很多人很讨厌理论知识。但是正是因为这些我们所讨厌理论知识让我们可以更好的去做事情。理解其原理才能更好的解决问题。

下面所写的内容基于个人理解,如有理解错误请各位指正,谢谢。

为什么需要垃圾回收机制

为什么需要垃圾回收机制,如果没有垃圾回收机制,我们必须去手动分配内存和释放内存.

例如c++,但是很多时候我们分配了内存但是没有去释放内存导致应用卡顿崩溃,作者大一的时候学c++,课设学生成绩管理系统,那时候写代码的时候需要手动去分配内存,还需要手动释放,很多时候还忘记去写内存释放逻辑,导致作者帮其他同学做的管理系统演示着的时候就崩溃了。

而有了垃圾回收机制之后我们就不需要去手动释放内存,垃圾回收机制会自动去释放不需要的内存。

分代式垃圾回收机制

垃圾回收机制只考虑堆内存,而像栈内存存储简单数据类型的的格子占用会在函数执行完后自动释放。

空间划分

把堆内存分为新生代空间、老生代空间。

  • 新生代空间存储刚创建的、存活率低的、内存占用小的对象。
  • 老生代空间存储存活率高的、内存占用大的对象。

流程

新生代空间分为使用区和空闲区,当垃圾回收机制执行的时候,会执行Scavenge算法来回收释放内存。

  • 会去递归遍历使用区内存中根对象,遍历到就把对象的某位标志二进制由0 -> 1

  • 把标志为0的失活对象进行内存释放。

  • 把使用区的的对象进行自动排序,往一端移动,把边界外的内存释放。这样是为了解决内存碎片,这个下文会讲

  • 把每个存活对象的标志二进制0->1

  • 把使用区的对象复制到空闲区,再置换使用区和空闲区。

  • 当使用区当中经历过多次Scavenge算法的对象复制到老生代空间,当然还有一种情况是使用区内存占用率达到了25%的时候也会进行这个操作。至于原因是当内存占用率过高的时候会影响内存分配。

什么是分代式

因为堆内存分为新生代空间和老生代空间。

为什么会划分成新生代空间和老生代空间?

这是因为

新生代空间存储刚创建的、存活率低的、内存占用小的对象。

老生代空间存储存活率高的、内存占用大的对象。

如果不划分空间,那么每次都要扫一遍堆内存,检测频率一样的话,会对性能造成很大的影响。这是一个优化点。

怎么标记失活对象和非失活对象

引用计数

引用计数其实就是但a引用b时就把b的引用计数+1,当引用计数为0时就会自动释放。

  • 优点 逻辑简单
  • 缺点 1.要一个很大内存空间去存储计数 2.存在解决循环引用的问题

标记清除法

标记清除就是去递归遍历每个根对象,如果遍历到就把标记为1,那么遍历不到的对象标记就是初始值0,那么就可以把标记为0的失活对象给清除。

  • 优点 1.解决了循环引用的问题 2.不用花费额外的内存空间去存储计数
  • 缺点:需要去递归遍历根对象

内存碎片整理

说内存碎片问题时,我们先说一下内存分配策略

  • 遍历空闲表,找到大小大于等于n的分坤,马上返回分配
  • 遍历空闲表,找到大小大于等于n的最小分块分配
  • 遍历空闲表,找到大小大于等于n的最大内存分块切割分配n

当内存释放,会产生很多间隔不连续的空闲内存块,现在普遍按第一种进行内存分配。那么就导致很多内存浪费。 所以为了解决这个问题,当内存释放后,会让对象往一端移动,然后清除边界外的空间。

全卡顿问题

js其实是在虚拟机主线程执行的,那么执行垃圾回收机制的时候,应用就必须挂起,这样的话当垃圾回收机制执行时间长的话就会造成应用卡顿。所以就有了一种优化方式增量标记。

增量标记

增量标记其实就是把标记过程分块执行,与应用交替执行。这样看似解决了全卡顿的问题但是却诞生了两个问题

  • 应用执行解决改变已标记的对象的引用
  • 增量标记分块标记还是在主线程执行

增量标记分块标记还是在主线程执行

并发回收

利用多个辅助线程进行标记,但是实际过程还是会占用主线程。

并行回收

利用多个辅助线程后台进行标记,但是不会占用主线程

解决应用执行改变已标记的对象的引用

按之前的策略,也就是0或1的策略,那么久会导致一个问题诞生。 a、b两个对象,a引用了b,那么改变a成员变量,引用c。那么就会导致一个问题,就是此时a为1,b为1,但是c为0,但是此时a已经遍历完了,那么c会被认为失活对象,被回收。

为了解决这个问题,就产生了一个三色标记法

三色标记法

因为如果采用一位二进制位标记,那么就无法知道标记到哪里了,所以为了解决这个问题就产生了三色标记法把标记增加到两位

  • 0 为遍历对象
  • 1 遍历完对象及其成员变量
  • 10 遍历对象,但未遍历成员变量

三色标记法借助标记表去实现标记,遍历根对象时会把对象推入标记表,遍历该对象,弹出该对象,该对象变为1,成员变量为10。再把10对象入表遍历,当没有10对象时就解决标记,其他为0的对象就为失活对象。这是三色标记法的过程。

写屏障

因为是多线程增量标记,那么就会产生当应用执行改变已标记的对象的引用的问题,那么就产生写屏障当应用执行改变已标记的对象的引用,那么把就新引用的对象变成10,继续入标志表。

惰性清理

垃圾回收机制内存清理其实是一种惰性清理。会每个对象每个对象清理,直至清理完所有失活对象。当前v8采用以上多种形式进行垃圾回收。

内存泄漏检测

怎么去检测内存泄漏问题,先说一下大概导致内存泄漏的问题,定时器无用后没有取消,dom对象移出dom树后为清除引用、注册的事件无用未取消、利用多个匿名函数重复监听一个事件、闭包等等。我们这里只分析内存其实是分析堆内存。因为栈内存存储的基本数据类型,在使用完就会释放。

我们下面从一个例子介绍怎么使用开发者工具去检测。从推荐tab切换到最新tab再切换回推荐tab

开发者工具-性能

  • 手动触发一次gc
  • 然后点击录制
  • 操作
  • 手动触发一次gc
  • 停止录制

image.png

我们可以看到开始值和结果值其实相差不大,所以没什么内存泄漏问题。我们还可以给图表增加相应的指标,还能看到目前cpu的使用情况以及js堆大小。

开发工具-内存

image.png

我们可以通过1和2去进行录制,看到操作前和操作后的一个情况。

时间轴上的分配插桩

image.png 我们可以通过时间轴看到。当我们操作的时候哪个点创建了哪些对象。

堆快照

堆快照我们要记录两次,一次是操作前,一次是操作后,再对比数据。

image.png

写在结尾

文章写的比较粗糙,很多内容想写出来,但是越写越多,最后不知该怎么写了。对有些概念其实我也只停留在表层,如果有一些点写的有问题,麻烦指出。谢谢。

人随着时间,不断的在成长。很多以前不敢想的事情,现在正在一步一步踏出。有很多朋友会问人生的意义问题。我以前也总会问。现在想想最后不过是消散于世间,有什么大不了的。你时常烦恼。不过是你心里存在着大量不容易实现的欲望在烦恼着你。

猜你喜欢

转载自juejin.im/post/7110867248520593439