一.执行环境
执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的一个执行环境。在web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个环执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的变量和函数定义也随之销毁(全局执行环境直到应用程序退出—例如关闭网页或浏览器时才会被销毁)
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
二.作用域
作用域链:当代码在一个环境中执行时,会创建变量对象的一个作用域链。
用途:保证对执行环境有权访问的所有变量和函数的有序访问。
在标识符解析的时候,始终按照从当前环境开始,一层一层想外层,直到找到标识符
三.没有块级作用域
这个很有意思,在Javascript中没有块级作用域,**只有函数作用域,**怎么理解这句话?
块级作用域,顾名思义就是{}花括号括起来的,是一个块,那这句话的意思就是,即使在{}中定义的变量,在{}之外也能访问的到,举例
for(var i=0;i<=10;i++){
// console.log(i);
}
console.log(i); 11
if(true){
var a = 1;
}
console.log(a); 1
那怎么理解有函数作用域呢?就是在函数内定义的变量,在函数外无法访问。
四.声明变量
使用var声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境,如果初始化变量时,没有使用var声明,该变量会自动被添加到全局环境.
注意看和上面的例子有什么不同,没有使用var关键字进行声明,所以变量被自动添加到全局环境中了,所以我们在写代码的时候一定要注意先声明再使用哦。
五.let,var const的区别(有需要的小伙伴去看吧)
五.垃圾回收
JavaScript具有自动的垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。原理就是:找到那些不再使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔周期性的执行这一操作。
下面我们来分析一下函数中局部变量的正常生命周期。局部变量只在函数执行的过程中存在。在这个过程中,会为局部变量在栈或者堆内存中分配空间,以便存储它们的值,然后在函数中使用这些变量,直至函数执行结束。此时局部变量就没有存在的必要了,因此可以释放他们的内存以便将来使用。
5.1标记清除
JavaScript中最常用的垃圾收集方式是标记清除。
当变量进入环境(如:在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
垃圾收集器会运行的时候,
1.会给存储在内存中的所有的变量都加上标记,假设内存中所有的对象全部都是垃圾,标记为0.
2.去掉环境中的变量和被环境中的变量引用的变量的标记,把不是垃圾的节点改为1.
3.在此之后再被加上标记的变量将被视为准备删除的变量
4.销毁那些带标记的值并回收它们锁占用的内存空间,清理所有标记为0的垃圾。
优点:
标记清楚算法的优点是比较简单,打标记只有打与不打,这使用二进制0和1,即可实现
缺点:
标记清除算法在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片(如下图)。
假设我们新建对象分配内存时需要大小为 size
,由于空闲内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于 size
的块才能为其分配(如下图)
那如何找到合适的块呢?我们可以采取下面三种分配策略
- First-fit,找到大于等于size的块立即返回
- Best-fit,遍历整个空闲列表,返回大于等于size的最小分块
- Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size大小,并将该部分返回
缺点:
1.内存碎片化:空间内存块是不连续的,容易出现很多空闲内存块
2.分配速度慢:因为即便是使用 First-fit
策略,其操作仍是一个 O(n)
的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
PS:标记清除算法的缺点补充
归根结底,标记清除算法的缺点在于清除之后剩余的对象位置不变而导致的空闲内存不连续,所以只要解决这一点,两个缺点都可以完美解决了
而 标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
5.2引用计数
1.声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是1。
2.同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1.
3.当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数-1.
4.当引用次数变成0时,说明没办法访问这个值了。
5.当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。
V8对GC的优化
现在大多数浏览器都是基于标记清除算法,V8也是,当然V8也对其进行了一些优化加工处理,接下来我们看一看V8对垃圾回收机制的优化
1.分代式垃圾回收
试想一下,我们上面所说的垃圾清理算法在每次垃圾回收的时候都要检查内存中所有的对象,这样的话对于一些大,老,存活时间长的对象来说同新,小,存活时间短的对象的一个频率检查很不好,因为前者需要时间长并且不需要频繁进行清理,后者恰好相反,怎么优化这一点呢?分代式来了
新老生代
V8的垃圾回收机子hi主要基于分代式垃圾回收机制,V8将堆内存分为新生代和老生代两部分,采用不同的垃圾回收器也就是不同的策略管理垃圾回收。
新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持1~8M的容量,而老生代的对象为存活时间较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大。
V8整个堆内存的大小等于新生代内存加上老生代内存
对于新老两块内存的垃圾回收,V8采用了两个垃圾处理器来管控。
新生代垃圾回收
新生代垃圾回收是通过一个名为Scavenge
的算法进行垃圾回收,在Scavenge算法的具体实现中,主要采用了一种复制式的方法,即Cheney算法
Cheney算法中将堆内存一分为二,一个处于使用状态的空间,我们暂且称之为使用区
,另一个处于闲置状态,我们称之为空闲区
新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作
当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。最后进行角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区
当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理
另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge
回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配
老生代垃圾回收
首先是标记阶段,从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象
清除阶段老生代垃圾回收器会直接将非活动对象,也就是数据清理掉
前面我们也提过,标记清除算法在清除后会产生大量不连续的内存碎片,过多的碎片会导致大对象无法分配到足够的连续内存,而 V8 中就采用了我们上文中说的标记整理算法来解决这一问题来优化空间
2.并行回收
它指的是主线程在执行Javascript的过程中,辅助线程能够在后台执行垃圾回收的操作,辅助线程在这执行垃圾回收的时候,主线程也可以自由执行而不会被挂起。
再说V8中GC优化
V8 的垃圾回收策略主要基于分代式垃圾回收机制,这我们说过,关于新生代垃圾回收器,我们说使用并行回收可以很好的增加垃圾回收的效率,那老生代垃圾回收器用的哪个策略呢?我上面说了并行回收、增量标记与惰性清理、并发回收这几种回收方式来提高效率、优化体验,看着一个比一个好,那老生代垃圾回收器到底用的哪个策略?难道是并发??内心独白:” 好像。。貌似。。并发回收效率最高 “
其实,这三种方式各有优缺点,所以在老生代垃圾回收器中这几种策略都是融合使用的
老生代主要使用并发标记,主线程在开始执行 JavaScript
时,辅助线程也同时执行标记操作(标记操作全都由辅助线程完成)
标记完成之后,再执行并行清理操作(主线程在执行清理操作时,多个辅助线程也同时执行清理操作)
同时,清理的任务会采用增量的方式分批在各个 JavaScript
任务之间执行
注:本文为笔者学习记录,大部分为摘录,详情请看下面的文章,有错误请指出。
参考文章:
《JavaScript高级程序设计》第4章