一. 什么是垃圾、为什么要回收
1. 什么是垃圾
在程序执行的过程中,只要程序提出请求如定义变量、定义常量等,操作系统或运行时都会给我们分配内存空间。程序执行过程中会产生不再使用的变量、常量等,我们就称为垃圾。
2. 为什么要回收
如果不进行垃圾回收,我们的内存被占用情况将会越来越严重,导致后续的程序执行得不到足够的内存空间,轻则影响系统性能,重则导致程序崩溃。
3. 如何回收
js、java、python、等都是采用程序搭载了GC(垃圾回收)算法进行自动回收。从申请内存空间、使用内存空间、释放内存空间,GC算法会帮我们自动完成。
二. 垃圾回收算法要做什么事情
- 找到内存空间的垃圾
- 回收垃圾,让程序可以再次利用这部分空间
三. 垃圾回收算法相关概念
-
对象/头/域
- 对象:通过应用程序创建的数据的集合,数据存储在内存空间里。对象由头Header和域field组成。
- 头:保存对象本身信息的部分称之为头Header,如对象的大小、对象的种类,对象使用者无法更改头信息
- 域:对象使用者在可访问的部分称之为域,个人理解就是对象的值。对象使用者可以更改域信息。域中数据包含指针和非指针类,非指针就是值本身(可以理解为基础数据类型)、指针指向内存中模块区域也就是其他对象的地址。
- 指针:指针是指对象的域中存储的是一个指向其他对象的地址
- 活动对象/非活动对象:从根出发,可以访问的是活动对象,否则是非活动对象
- 分配:当程序创建对象时,向分配器申请,分配器寻找到合适的符合要求的空间并将空间地址返回给程序
- 分块:初始状态下,堆被一个大的分块所占据。然后,程序会根据 应用程序的要求把这个分块分割成合适的大小,作为(活动)对象使用。活动对象不久后会转化为垃圾被回收。此时,这部分被回收的内存空间再次成为分块,为下次被利用做准备。也就是说,内存里的各个区块都重复着分块→活动对象→垃圾(非活动对象)→分块→ …… 这样的过程。
- 根:root,根是指向对象的指针的起点,浏览器环境是window,node环境是global
- 垃圾回收时应用程序处于暂停状态
- 堆与栈:
内存 | 数据结构 | |
---|---|---|
堆 | new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。由程序员分配和回收 | 是一棵完全二叉树结构 |
栈 | 存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。 | 是一种连续存储的数据结构,特点是存储的数据先进后出 |
四. 垃圾回收算法
4.1 标记-清除
1. 标记清除Mark Sweep GC,1960年提出此概念,最早出现的GC算法。整个过程由标记+清除,两个阶段组成。
- 标记阶段:从根出发遍历所有能够从根可达的对象,将其标记为活动对象
- 清除阶段:将未标记的非活动对象进行统一回收
2. 什么是可达对象?
从根出发,如果是浏览器就是从window出发,如果是node则是从global出发,可以访问到的就是可达对象。
3. 遍历对象采用的什么算法?怎么实现遍历所有对象?
遍历对象可以采用两种算法,深度优先算法和广度优先算法
- 深度优先算法:Depth-First-Search,简称DFS
主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底...,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。
所以最终的执行顺序是:1-2-5-9-3-6-10-7-4-8
- 广度优先算法:Breath First Search,简称BFS
主要思路是指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。
动态效果可参考如下链接:
https://s4.51cto.com/oss/202004/16/a2c7c61edcadffeed85c10f53f1c988c.gif
最终的执行顺序是1-2-3-4-5-6-7-8-9-10
广度优先算法采用队列实现
4. 缺点:
- 效率问题:标记和清除环节因为需要找到所有可达对象,所以效率并不高
- 空间碎片化:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至导致应用程序暂停。
4.2 引用计数
- 算法实现思路:通过在对象头中分配一个空间来记录该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被立刻回收。
示例:let a = 'abc',则abc的引用计数为1
当我们执行a=null时,abc的引用计数就变为了0,此时会立即触发GC
- 优点:
- 即可回收垃圾:每个对象都有自己的计数器,一旦计数器为0立刻进行垃圾回收
- 最大暂停时间短:因为垃圾的即时回收,会大幅缩减程序的暂停时间
- 没必要沿指针查找:与标记清除算法不一样,没必要由根出发沿指针查找
- 缺点:
- 计数器的增减比较繁重:
- 计数器需要占用很多位:
- 循环引用无法回收 a引用b,b引用a,这种情况始终无法回收
4.3 复制算法
- 算法实现思路:copying GC,把某个空间里的活动对象复制到其他空间,把原空间里的所有对象都回收掉。原空间称为From空间,粘贴活动对象的新空间称为To空间。当From空间完全被占满时,GC会将活动对象全部复制到To空间,当复制完成后,该算法会把From空间和To空间互换,GC也就结束了。From和To空间大小必须一致。
- 优点:
- 不会发生碎片化:每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况
- 优秀的吞吐量:只搜索从根出发的活动对象并只复制活动对象
- 缺点:
- 堆使用效率低下:GC复制算法把堆分为对等的两份,通常只能利用其中的一半来安排对象。也就是只有一半堆被使用
- 效率问题:在对象存活率较高时,复制操作次数多,效率降低;
4.4 标记整理
- 算法实现思路
分为标记和整理两个阶段:首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。不会立即进行垃圾回收
- 优点:不会产生空间碎片化
- 缺点:整理内存空间需要花费一定的时间
4.5 分代回收
- 算法实现思路:
分代回收的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
“分代收集”算法,把堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
- 优点:因为针对不同的情况采用不同的GC算法,所以它的吞吐量得到了改善
- 缺点:在部分程序中会起到反作用,我们采用分代垃圾回收基于一个认知:很多对象年纪轻轻就称为了垃圾,如果程序中对象都活的很久,会造成两个问题(1)新生代GC花费的时间变多(2)老年代频繁GC
五. 认识V8的垃圾回
1. 认识V8
- v8是一款主流的js执行引擎
- v8采用即时编译(速度很快)
- v8内存设限:64位不超过1.5G,32位不超过800M
为什么会设置为1.5G和800M?V8官方进行过一个测试:1.5G的垃圾,采用增量标记算法需要50毫秒,如果采用的是非增量回收需要1秒,1秒的停顿客户已有感知,如果垃圾大于1.5G会影响用户体检,故而将内存设置为1.5G
2. V8垃圾回收策略
1. 采用分代回收的思想、将内存安装一定的规则分为新生代内存区、老生代内存区,针对不同的内存区采用不同的垃圾回收机制
2. v8常用的GC算法:
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量(为了提高效率)
3. V8如何回收新生代对象
1. 将内存分为大小相同的两块:新生代内存块中存放新生代对象也就是存活时间较短的对象(比如局部变量),老生代内存中存放老生代对象也就是存货时间教长的对象
2. 64位系统中新生代内存的大小为32M,32位系统中新生代内存大小为16M
3. 新生代对象采用的GC算法主要有复制算法+标记整理
4. 新生代缓存区:分为两个大小相同的区域from和to.
from:使用状态,在声明变量时都会进入到from空间,当from使用达到一定的程度触发GC,首先进行活动对象的标记,不可达对象不标记,然后进行整理,将不连续的内存空间整理为连续内存空间。然后将活动对象从from拷贝到to中,然后将from完全释放。
to: 空闲状态
5. 拷贝过程中存在晋升:晋升就是将新生代内存区中的对象转移至老生代对象
出现晋升的时机有:
(1)一轮GC之后还存活的对象需要晋升
(2)to空间的使用率超过25%将所有对象转移至老生代空间,设置25%这个阈值的原因是当这次回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配
4. V8如何回收老生代对象
- 64位系统中老生代内存的大小为1.4G,32位系统中新生代内存大小为700M
- 老年代对象就是指存活时间比较长的对象如全局变量、闭包等
- 老年代对象主要采用标记清除、标记整理、增量标记算法,主要使用标记清除,但会产生空间碎片化问题,即使这样,V8底层针对老年代对象也主要使用的是此种GC算法(效率高)。
- 什么时候用标记整理?当我们想要把新生代存储区的内容挪到老生代存储区中,但老生代存储区的内存空间不能满足,此时就会进行一轮标记整理对碎片化空间进行整理,以释放更多的空间
5. 标记增量如果进行垃圾回收?
因为垃圾回收会导致程序暂停。将一整段回收操作分为多个阶段进行,以方便和程序交互进行。
参考链接:cnblogs.com/xiaoyuxy/p/12258874.htm
参考书籍:《垃圾回收的算法与实现》