浅谈Chrome V8引擎中的垃圾回收机制
垃圾回收器
JavaScript的垃圾回收器
JavaScript使用垃圾回收机制来自动管理内存。垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带来的内存泄露问题。但使用了垃圾回收即意味着程序员将无法掌控内存。ECMAScript没有暴露任何垃圾回收器的接口。我们无法强迫其进 行垃圾回收,更无法干预内存管理
内存管理问题
在浏览器中,Chrome V8引擎实例的生命周期不会很长(谁没事一个页面开着几天几个月不关),而且运行在用户的机器上。如果不幸发生内存泄露等问题,仅仅会 影响到一个终端用户。且无论这个V8实例占用了多少内存,最终在关闭页面时内存都会被释放,几乎没有太多管理的必要(当然并不代表一些大型Web应用不需 要管理内存)。但如果使用Node作为服务器,就需要关注内存问题了,一旦内存发生泄漏,久而久之整个服务将会瘫痪(服务器不会频繁的重启)
Chrome的内存限制
存在限制
Chrome限制了所能使用的内存极限(64位为1.4GB,32位为1.0GB),这也就意味着将无法直接操作一些大内存对象。
为何限制
Chrome之所以限制了内存的大小,表面上的原因是V8最初是作为浏览器的JavaScript引擎而设计,不太可能遇到大量内存的场景,而深层次的原因 则是由于V8的垃圾回收机制的限制。由于V8需要保证JavaScript应用逻辑与垃圾回收器所看到的不一样,V8在执行垃圾回收时会阻塞 JavaScript应用逻辑,直到垃圾回收结束再重新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。 若V8的堆内存为1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。这样浏览器将在1s内失去对用户的响 应,造成假死现象。如果有动画效果的话,动画的展现也将显著受到影响
Chrome V8的堆构成
V8的堆其实并不只是由老生代和新生代两部分构成,可以将堆分为几个不同的区域:
* 新生代内存区:大多数的对象被分配在这里,这个区域很小但是垃圾回特别频繁
* 老生代指针区:属于老生代,这里包含了大多数可能存在指向其他对象的指针的对象,大多数从新生代晋升的对象会被移动到这里
* 老生代数据区:属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针
* 大对象区:这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象
* 代码区:代码对象,也就是包含JIT之后指令的对象,会被分配在这里。唯一拥有执行权限的内存区
* Cell区、属性Cell区、Map区:存放Cell、属性Cell和Map,每个区域都是存放相同大小的元素,结构简单
每个区域都是由一组内存页构成,内存页是V8申请内存的最小单位,除了大对象区的内存页较大以外,其他区的内存页都是1MB大小,而且按照1MB对
齐。内存页除了存储的对象,还有一个包含元数据和标识信息的页头,以及一个用于标记哪些对象是活跃对象的位图区。另外每个内存页还有一个单独分配在另外内
存区的槽缓冲区,里面放着一组对象,这些对象可能指向其他存储在该页的对象。垃圾回收器只会针对新生代内存区、老生代指针区以及老生代数据区进行垃圾回收
Chrome V8的垃圾回收机制
如何判断回收内容
如何确定哪些内存需要回收,哪些内存不需要回收,这是垃圾回收期需要解决的最基本问题。我们可以这样假定,一个对象为活对象当且仅当它被一个根对象
或另一个活对象指向。根对象永远是活对象,它是被浏览器或V8所引用的对象。被局部变量所指向的对象也属于根对象,因为它们所在的作用域对象被视为根对
象。全局对象(Node中为global,浏览器中为window)自然是根对象。浏览器中的DOM元素也属于根对象
如何识别指针和数据
垃圾回收器需要面临一个问题,它需要判断哪些是数据,哪些是指针。由于很多垃圾回收算法会将对象在内存中移动(紧凑,减少内存碎片),所以经常需要进行指针的改写
目前主要有三种方法来识别指针:
1. 保守法:将所有堆上对齐的字都认为是指针,那么有些数据就会被误认为是指针。于是某些实际是数字的假指针,会背误认为指向活跃对象,导致内存泄露(假指针指向的对象可能是死对象,但依旧有指针指向——这个假指针指向它)同时我们不能移动任何内存区域。
2. 编译器提示法:如果是静态语言,编译器能够告诉我们每个类当中指针的具体位置,而一旦我们知道对象时哪个类实例化得到的,就能知道对象中所有指针。这是JVM实现垃圾回收的方式,但这种方式并不适合JS这样的动态语言
3. 标记指针法:这种方法需要在每个字末位预留一位来标记这个字段是指针还是数据。这种方法需要编译器支持,但实现简单,而且性能不错。V8采用的是这种方式。V8将所有数据以32bit字宽来存储,其中最低一位保持为0,而指针的最低两位为01
V8的回收策略
自动垃圾回收算法的演变过程中出现了很多算法,但是由于不同对象的生存周期不同,没有一种算法适用于所有的情况。所以V8采用了一种分代回收的策
略,将内存分为两个生代:新生代和老生代。新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。分别对新生代和老生代使用
不同的垃圾回收算法来提升垃圾回收的效率。对象起初都会被分配到新生代,当新生代中的对象满足某些条件(后面会有介绍)时,会被移动到老生代(晋升)
V8的分代内存
默认情况下,64位环境下的V8引擎的新生代内存大小32MB、老生代内存大小为1400MB,而32位则减半,分别为16MB和700MB。V8内存的最大保留空间分别为1464MB(64位)和732MB(32位)。具体的计算公式是4*reserved_semispace_space_ + max_old_generation_size_
,新生代由两块reserved_semispace_space_
组成,每块16MB(64位)或8MB(32位)
新生代
新生代的特点
大多数的对象被分配在这里,这个区域很小但是垃圾回特别频繁。在新生代分配内存非常容易,我们只需要保存一个指向内存区的指针,不断根据新对象的大小进行递增即可。当该指针到达了新生代内存区的末尾,就会有一次清理(仅仅是清理新生代)
新生代的垃圾回收算法
新生代使用Scavenge算法进行回收。在Scavenge算法的实现中,主要采用了Cheney算法。
Cheney算法算法是一种采用复制的方式实现的垃圾回收算法。它将内存一分为二,每一部分空间称为semispace。在这两个
semispace中,一个处于使用状态,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空
间,当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收算法时,会检查From空间中的存活对象,这些存活对象将会被复制到To空间中
(复制完成后会进行紧缩),而非活跃对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。也就是说,在垃圾回收的过程中,就是
通过将存活对象在两个semispace之间进行复制。可以很容易看出来,使用Cheney算法时,总有一半的内存是空的。但是由于新生代很小,所以浪费
的内存空间并不大。而且由于新生代中的对象绝大部分都是非活跃对象,需要复制的活跃对象比例很小,所以其时间效率十分理想。复制的过程采用的是BFS(广
度优先遍历)的思想,从根对象出发,广度优先遍历所有能到达的对象
具体的执行过程大致是这样:
首先将From空间中所有能从根对象到达的对象复制到To区,然后维护两个To区的指针scanPtr和allocationPtr,分别指向即将
扫描的活跃对象和即将为新对象分配内存的地方,开始循环。循环的每一轮会查找当前scanPtr所指向的对象,确定对象内部的每个指针指向哪里。如果指向
老生代我们就不必考虑它了。如果指向From区,我们就需要把这个所指向的对象从From区复制到To区,具体复制的位置就是allocationPtr
所指向的位置。复制完成后将scanPtr所指对象内的指针修改为新复制对象存放的地址,并移动allocationPtr。如果一个对象内部的所有指针
都被处理完,scanPtr就会向前移动,进入下一个循环。若scanPtr和allocationPtr相遇,则说明所有的对象都已被复制完,From
区剩下的都可以被视为垃圾,可以进行清理了
举个栗子(以及凑篇幅),如果有类似如下的引用情况:
+----- A对象
|
根对象----+----- B对象 ------ E对象
|
+----- C对象 ----+---- F对象
|
+---- G对象 ----- H对象
D对象
在执行Scavenge之前,From区长这幅模样
+---+---+---+---+---+---+---+---+--------+
| A | B | C | D | E | F | G | H | |
+---+---+---+---+---+---+---+---+--------+
那么首先将根对象能到达的ABC对象复制到To区,于是乎To区就变成了这个样子:
allocationPtr
↓
+---+---+---+----------------------------+
| A | B | C | |
+---+---+---+----------------------------+
↑
scanPtr
接下来进入循环,扫描scanPtr所指的A对象,发现其没有指针,于是乎scanPtr移动,变成如下这样
allocationPtr
↓
+---+---+---+----------------------------+
| A | B | C | |
+---+---+---+----------------------------+
↑
scanPtr
接下来扫描B对象,发现其有指向E对象的指针,且E对象在From区,那么我们需要将E对象复制到allocationPtr所指的地方并移动allocationPtr指针:
allocationPtr
↓
+---+---+---+---+------------------------+
| A | B | C | E | |
+---+---+---+---+------------------------+
↑
scanPtr
B对象里所有指针都已被复制完,所以移动scanPtr:
allocationPtr
↓
+---+---+---+---+------------------------+
| A | B | C | E | |
+---+---+---+---+------------------------+
↑
scanPtr
接下来扫描C对象,C对象中有两个指针,分别指向F对象和G对象,且都在From区,先复制F对象到To区:
allocationPtr
↓
+---+---+---+---+---+--------------------+
| A | B | C | E | F | |
+---+---+---+---+---+--------------------+
↑
scanPtr
然后复制G对象到To区
allocationPtr
↓
+---+---+---+---+---+---+----------------+
| A | B | C | E | F | G | |
+---+---+---+---+---+---+----------------+
↑
scanPtr
这样C对象内部的指针已经复制完成了,移动scanPtr:
allocationPtr
↓
+---+---+---+---+---+---+----------------+
| A | B | C | E | F | G | |
+---+---+---+---+---+---+----------------+
↑
scanPtr
逐个扫描E,F对象,发现其中都没有指针,移动scanPtr:
allocationPtr
↓
+---+---+---+---+---+---+----------------+
| A | B | C | E | F | G | |
+---+---+---+---+---+---+----------------+
↑
scanPtr
扫描G对象,发现其中有一个指向H对象的指针,且H对象在From区,复制H对象到To区,并移动allocationPtr:
allocationPtr
↓
+---+---+---+---+---+---+---+------------+
| A | B | C | E | F | G | H | |
+---+---+---+---+---+---+---+------------+
↑
scanPtr
完成后由于G对象没有其他指针,且H对象没有指针移动scanPtr:
allocationPtr
↓
+---+---+---+---+---+---+---+------------+
| A | B | C | E | F | G | H | |
+---+---+---+---+---+---+---+------------+
↑
scanPtr
此时scanPtr和allocationPtr重合,说明复制结束
可以对比一下From区和To区在复制完成后的结果:
//From区
+---+---+---+---+---+---+---+---+--------+
| A | B | C | D | E | F | G | H | |
+---+---+---+---+---+---+---+---+--------+
//To区
+---+---+---+---+---+---+---+------------+
| A | B | C | E | F | G | H | |
+---+---+---+---+---+---+---+------------+
D对象没有被复制,它将被作为垃圾进行回收
写屏障
如果新生代中的一个对象只有一个指向它的指针,而这个指针在老生代中,我们如何判断这个新生代的对象是否存活?为了解决这个问题,需要建立一个列表用来记录所有老生代对象指向新生代对象的情况。每当有老生代对象指向新生代对象的时候,我们就记录下来
对象的晋升
当一个对象经过多次新生代的清理依旧幸存,这说明它的生存周期较长,也就会被移动到老生代,这称为对象的晋升。具体移动的标准有两种:
1. 对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一个新生代的清理,如果是,则复制到老生代中,否则复制到To空间中
2. 对象从From空间复制到To空间时,如果To空间已经被使用了超过25%,那么这个对象直接被复制到老生代
老生代
老生代的特点
老生代所保存的对象大多数是生存周期很长的甚至是常驻内存的对象,而且老生代占用的内存较多
老生代的垃圾回收算法
老生代占用内存较多(64位为1.4GB,32位为700MB),如果使用Scavenge算法,浪费一半空间不说,复制如此大块的内存消耗时间将
会相当长。所以Scavenge算法显然不适合。V8在老生代中的垃圾回收策略采用Mark-Sweep和Mark-Compact相结合
Mark-Sweep(标记清除)
标记清除分为标记和清除两个阶段。在标记阶段需要遍历堆中的所有对象,并标记那些活着的对象,然后进入清除阶段。在清除阶段总,只清除没有被标记的对象。由于标记清除只清除死亡对象,而死亡对象在老生代中占用的比例很小,所以效率较高
标记清除有一个问题就是进行一次标记清楚后,内存空间往往是不连续的,会出现很多的内存碎片。如果后续需要分配一个需要内存空间较多的对象时,如果所有的内存碎片都不够用,将会使得V8无法完成这次分配,提前触发垃圾回收。
Mark-Compact(标记整理)
标记整理正是为了解决标记清除所带来的内存碎片的问题。标记整理在标记清除的基础进行修改,将其的清除阶段变为紧缩极端。在整理的过程中,将活着的
对象向内存区的一段移动,移动完成后直接清理掉边界外的内存。紧缩过程涉及对象的移动,所以效率并不是太好,但是能保证不会生成内存碎片
算法思路
标记清除和标记整理都分为两个阶段:标记阶段、清除或紧缩阶段
在标记阶段,所有堆上的活跃对象都会被标记。每个内存页有一个用来标记对象的位图,位图中的每一位对应内存页中的一个字。这个位图需要占据一定的空
间(32位下为3.1%,64位为1.6%)。另外有两位用来标记对象的状态,这个状态一共有三种(所以要两位)——白,灰,黑:
* 如果一个对象为白对象,它还没未被垃圾回收器发现
* 如果一个对象为灰对象,它已经被垃圾回收器发现,但其邻接对象尚未全部处理
* 如果一个对象为黑对象,说明他步进被垃圾回收器发现,其邻接对象也全部被处理完毕了
如果将对中的对象看做由指针做边的有向图,标记算法的核心就是深度优先搜索。在初始时,位图为空,所有的对象也都是白对象。从根对象到达的对象会背
染色为灰色,放入一个单独的双端队列中。标记阶段的每次循环,垃圾回收器都会从双端队列中取出一个对象并将其转变为黑对象,并将其邻接的对象转变为灰,然
后把其邻接对象放入双端队列。如果双端队列为空或所有对象都变成黑对象,则结束。特别大的对象,可能会在处理时进行分片,防止双端队列溢出。如果双端队列
溢出,则对象仍然会成为灰对象,但不会被放入队列中,这将导致其邻接对象无法被转变为灰对象。所以在双端队列为空时,需要扫描所有对象,如果仍有灰对象,
将它们重新放入队列中进行处理。标记结束后,所有的对象都应该非黑即白,白对象将成为垃圾,等待释放
清除和紧缩阶段都是以内存页为单位回收内存
清除时垃圾回收器会扫描连续存放的死对象,将其变成空闲空间,并保存到一个空闲空间的链表中。这个链表常被scavenge算法用于分配被晋升对象的内存,但也被紧缩算法用于移动对象
紧缩算法会尝试将碎片页整合到一起来释放内存。由于页上的对象会被移动到新的页上,需要重新分配一些页。大致过程是,对目标碎片页中的每个活跃对
象,在空闲内存链表中分配一块内存页,将该对象复制过去,并在碎片页中的该对象上写上新的内存地址。随后在迁出过程中,对象的旧地址将会被记录下来,在迁
出结束后,V8会遍历所有它所记录的旧对象的地址,将其更新为新地址。由于标记过程中也记录了不同页之间的指针,这些指针在此时也会进行更新。如果一个页
非常活跃,如其中有过多需要记录的指针,那么地址记录会跳过它,等到下一轮垃圾回收进行处理
结合使用标记清除和标记整理
Chrome V8的老生代使用标记清除和标记整理结合的方式,主要采用标记清除算法,如果空间不足以分配从新生代晋升过来的对象时,才使用标记整理
浅谈Chrome V8引擎中的垃圾回收机制的更多相关文章
- Chrome V8系列--浅析Chrome V8引擎中的垃圾回收机制和内存泄露优化策略
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制.因此,V8 将内存(堆)分为新生代和老生代两部分. 一.前言 V8的垃圾回收机制:JavaScript使用垃圾回收机制来自动管理内存.垃圾 ...
- 浅谈V8引擎中的垃圾回收机制
最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...
- 浅谈surging服务引擎中的rabbitmq组件和容器化部署
1.前言 上个星期完成了surging 的0.9.0.1 更新工作,此版本通过nuget下载引擎组件,下载后,无需通过代码build集成,引擎会通过Sidecar模式自动扫描装配异构组件来构建服务引擎 ...
- V8 内存管理和垃圾回收机制总结
这篇文章主要介绍 V8 的内存管理和垃圾回收知识. V8 内存管理及垃圾回收机制浅析 由于 V8 引擎的原因,Node 在操作大内存对象时受到了一些限制,在 64 位的机器上,默认最大操作的对象大小约 ...
- JavaScript中的垃圾回收机制与内存泄露
什么是内存泄露? 任何编程语言,在运行时都需要使用到内存,比如在一个函数中, var arr = [1, 2, 3, 4, 5]; 这么一个数组,就需要内存. 但是,在使用了这些内存之后, 如果后面他 ...
- Python中的垃圾回收机制
Python的垃圾回收机制 引子: 我们定义变量会申请内存空间来存放变量的值,而内存的容量是有限的,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,而变量名是访问到变量值的唯一方式,所以 ...
- Javascript中的垃圾回收机制
Javascript 中的内存管理 译自MDN,Memory Management 简介 在底层语言中,比如C,有专门的内存管理机制,比如malloc() 和 free().而Javascript是有 ...
- Java中的垃圾回收机制
1. 垃圾回收的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象:而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾.JVM的 ...
- JAVA中的垃圾回收机制以及其在android开发中的作用
http://blog.csdn.net/xieqibao/article/details/6707519 这篇文章概述了JAVA中运行时数据的结构,以及垃圾回收机制的作用.在后半部分,描述了如何检测 ...
随机推荐
- Eclipse svn图标不显示
在菜单栏中:windows ->preferences->General->Appearance->Lable Decorations 勾选其中的 SVN 项,最后应用确认之后 ...
- Segments - POJ 3304 (判断直线与线段是否相交)
题目大意:给出一些线段,然后判断这些线段的投影是否有可能存在一个公共点. 分析:如果这些线段的投影存在一个公共点,那么过这个公共点作垂线一定与所有的直线都想交,于是题目转化成是否存在一个直线可以经 ...
- nyoj 32 组合数【简单dfs】
组合数 时间限制:3000 ms | 内存限制:65535 KB 难度:3 描述 找出从自然数1.2.... .n(0<n<10)中任取r(0<r<=n)个数的所有组合 ...
- 在Mapper中进行循环判断
1.在Bo中将值保存在list中
- Individual Contest #1 and Private Training #1
第一次的增补赛,也是第一场个人排位赛,讲道理打的和屎一样,手速题卡了好久还WA了好多发,难题又切不出来,这种情况是最尴尬的吧! Individual Contest #1: Ploblem D: 题意 ...
- string.Format字符串格式说明
先举几个简单的应用案例: 1.格式化货币(跟系统的环境有关,中文系统默认格式化人民币,英文系统格式化美元) string.Format("{0:C}",0.2) 结果为:¥0.20 ...
- 【设计模式 - 17】之中介者模式(Mediator)
1 模式简介 中介者模式的定义: 用一个中介者对象封装一系列的对象交互,中介者使各对象不需要显式地相互作用,从而使耦合松散,而且可以独立地改变它们之间的交互. 中介者模式中的组成部分: 1. ...
- <WEB>平板_手机开发_13 个处理触摸事件和多点触摸的JS 库
触摸屏是现在所有智能手机的标配,还包括各种平板设备,而且很多桌面也慢慢在开始支持触摸操作.要开发支持触摸屏设备的 Web 应用,我们需要借助浏览器的触摸事件来实线. 下图是各种触摸事件说明: 本文我们 ...
- linux下切割catalina.out文件,按天生成文件
1.下载工具cronolog压缩包(http://download.csdn.net/detail/sunling_sz/8144469) 2.将文件拖放到server,不论什么文件夹都能够. 3.进 ...
- Boost.Asio c++ 网络编程翻译(20)
异步服务端 这个图表是相当复杂的:从Boost.Asio出来你能够看到4个箭头指向on_accept.on_read,on_write和on_check_ping. 着也就意味着你永远不知道哪个异步调 ...