V8引擎简介

在这里我们先对引擎去做一些介绍和说明。

众所周知引擎是目前市面上最主流的,JavaScript执行引擎,我们日常所使用的chrome浏览器以及目前的NodeJavaScript平台呢都在采用这样的一个引擎去执行我们的JavaScript代码。

那么对于这两个平台来说,JavaScript之所以能在他们的上边去高效的运转,那么也正是因为这样一个幕后英雄的存在。

那这里呢就提到了JavaScript的一个高效的去转转,那么这也是我们V8的一个最大卖点, 那么这个速度之所以快,除了他的背后有一套优秀的内存管理机制之外呢,其实V8还有一个特点,就是采用及时编译。

那么之前很多的JavaScript引擎呢都需要将我们源代码先去转成我们的字节码,然后呢才能去执行,而对于V8来说呢就可以直接将源码呢给翻译成我们当前可以直接执行的机器码。

所以这个时候的速度呢是非常快的,那么接下来对于V8来讲,还有一个比较大的特点就是V8他的内存呢是设有上限的,那么这块我们先简单的说明一下。

对于V8来说呢他的内存空间去设置了一个数值,那么在64位的操作系统下,这个上限呢是不超过1.5G的,然后对于我们32位的操作系统来说呢,这个数值是不超过800M的。

那么为什么V8要采用这样的一个做法呢,原因基本上来说呢可以从两方面来进行总结。

第一呢V8本身呢就是为了浏览器而去制造的,所以,现有的这个一个内存大小对于网页应用来说呢,是足够使用了。

再有呢,V8内部所去实现的一个垃圾回收机制呢也决定了他采用这样的一个设置是非常合理的。

因为官方呢去做过这样的一个测试,当我们的垃圾内存呢去达到1.5个G的时候如果V8去采用增量标记的算法进行垃圾回收只需要消耗50ms。而如果采用非增量标记的形式去回收呢则需要1s。

那么偶从用户体验的角度来说呢,1s其实呢已经算是很长的时间了,所以在这里呢他就以1.5G为界了,那么对于V8内部的内存呢进行了一个上限的设置。

那这块呢就是针对于V8呢先做的一些简单的介绍,简单的总结一下就是,第一我们知道他是当前一个主流的JavaScript执行引擎,第二呢他的速度很快,因为他采用的是即时编译,第三呢,他的内部内存是有上限的,在64位操作系统下呢一般是不超过1.5G, 在32位的操作系统下呢是不超过800M的。

垃圾回收策略

在这里呢我们对于V8垃圾回收的策略呢进行一些介绍和说明,在这之前呢我们先做一些前置的描述。

我们都知道,在程序的使用过程中,那么我们会用到很多的数据,而这些数据呢我们又可以分为原始的数据和对象类型的数据。

那么对于这些基础的原始数据来说呢,他都是由程序的语言自身来进行控制的。所以在这里我们所提到的回收,那主要呢还是指的是当前存货在我们堆区里的对象数据。

因此,这个过程呢我们是离不开内存操作的,而我们当前也知道了在V8当中呢他对内存是做了上限的,所以这样的话,我们就想知道他是怎么样在这种情况下来对我们垃圾进行回收的。

那么这里呢我们就来具体的看一下,首先,对于V8来说呢他采用的是分代回收的一个思想,具体来说如何实现呢。

主要就是把我们当前的内存空间呢去按照一定的规则分成两类,一个呢就叫做新生代存储区,还有一个呢叫老生代存储区。

至于说如何去划分呢我们在后续会去介绍到,那有了这样一个分类之后,接下来他就会去针对于不同代然后呢去采用最高效的一种GC算法,从而呢去对不同的对象进行一个回收的操作。

那这里我们把这个过程简单的说明一下,首先在这里呢我们有一个V8的内存空间,然后他会去一分为二,一个呢用于存储新生代对象,一个呢用于存储老生代对象。

那紧接着我们再去针对于不同对象里边所存放的这样一些数据去采用具体的GC算法,所以这也就意味着,对于我们的V8回收来说呢,他会去使用到更多的GC算法。

因此在这里呢我们就先提前去做一些,整理和说明。

首先,对于我们的V8来说,分代回收这样的一个算法他肯定是要用到的,因为他必须要去做分代。

那紧接着呢他会去用到空间的复制算法,具体来说如何去做呢,我们后续进行介绍。

除此以外他还会用到我们之前所提到的标记清除和标记整理。

最后呢,为了去提高效率,所以在这里他们又用到了标记增量。

那么这个部分呢,就是关于V8当中的垃圾回收策略是如何执行的,我们在这呢做了一个描述,简单的整理一下就是,我们要记住V8的内存呢是有上限的,基于这样的一个条件,我们需要采用分代回收的思路,然后不同代的对象呢我们就再去采用更适合的一个GC算法, 从而呢去实现V8的一个搞笑垃圾回收的一个操作,那这里呢我们就说完了。

如何回收新生代对象

在这里我们来看一下V8当中是如何完成新生代存储区当中的垃圾回收操作。

首先在这个地方我们还是要去看一下V8内部的一个内存分配。因为他是基于分代的垃圾回收思想,所以在我们V8的内部是把内存空间分成了两个部分,我们内部的一个存储区域被分成了左右两个区域。

那么左侧的这样一个空间呢就是专门用来存放我们的新生代对象,而右侧呢就是专门用于存放我们的老生代对象。

那么在这里呢我们当前只是关注一下,新生代存储区的垃圾回收操作。所以在这我们先做一些文字上的说明。

跟刚才讲到的一样,我们当前的V8内部是把空间分成了两部分,而左侧呢专门去用于存储新生代对象,这样的一个空间呢是有一定设置的,那么在我们的64位操作系统当中他的大小呢是32M, 在32位的操作系统当中呢,就是16M。

那么有了这样一个空间之后,那么在他里边我们就可以存放相应的新生代对象,而这里的新生代对象呢,其实指的就是存活时间较短的,那么如何来界定存活时间较短呢?

举个例子,比如说我们在当前的这样一段代码内,有个局部的作用域,那么这个局部作用域当中的变量呢,在执行完成过后就肯定要去回收,而我们在其他的地方比如全局的地方呢也可能会有一个变量,而全局下方的这个变量呢他肯定要等到我们的程序退出之后才会被回收。

所以相对来说我们的新生代就指的是那些存活时间比较短的那样一些变量对象。

那么说完这块以后我们就具体的来看一下,在我们当前的V8当中是如何完成新生代对象回收的。

那么这块呢我们过程当中所采用到的算法呢主要就是复制算法和标记整理的算法操作。

首先他会去将我们当前左侧一部分小空间呢也去分成两个部分,叫做From和To,而且这两个部分的大小呢是相等的。

其中我们会去将From空间呢称之为叫做使用状态,然后To空间呢叫做空闲状态。

那么有了这样两个空间之后呢代码再执行的时候如果需要去申请空间来进行使用,首先呢他会将所有的变量对象呢都分配至From空间。

也就是说在这个过程当中,其实To呢是空闲着没有使用的,那么一旦当我们的From空间应用到一定的程度之后,那么就要去触发我们的GC操作。

所以这个时候呢他就会采用标记整理的操作,来对我们From空间进行一个活动对象的标记,那么找到这样一些活动对象之后,那么他继续呢去使用整理的操作,再去把他们的位置呢变得连续,也便于后续呢不会产生碎片化的空间。

那么昨晚这些操作以后,他会将这样的活动对象拷贝至我们的To空间,那么拷贝完成以后呢,就意味着我们之前From空间当中的活动对象就有了一个备份,所以这个时候呢,我们就可以考虑去做回收操作了。

至于说如何去回收呢,非常简单,我们只需要去把From的空间进行一个完全的释放就可以了,因为form里的对象在我们的To里边都有所体现。

所以这个时候我们吧From直接给他释放掉,不存在任何问题,那么这个过程也就完成了我们新生代对象的一个回收操作。

在这我们简答的总结一下就是,新生代对象的存储区域其实也被一分二位,而且是两个等大小的,在这两个等大小的空间中,我们起名From和To,那么当前我们使用的呢是From,所有的对象声明都会放在这样一个空间内。

然后触发GC机制的时候,我们就会去把这样的一个活动对象全部找到,然后进行整理,然后拷贝到我们的To空间当中。

拷贝完成以后我们再让这个From和To去进行空间的一个交换(也就是名字的交换),那么原来的To就变成了From,而原来的From就变成了To。

那这样一来我们就算是完成了这样一个空间的释放和回收操作。

那么接下来在这里边呢我们还要针对于这样的一个过程它里边的一些细节呢进行说明。

首先呢,在这个过程中我们肯定会想到的一个现象就是,如果我们在拷贝时,发现某一个变量对象所指用的空间呢,他在我们当前的老生代对象里面也会出现。

这个时候呢我们就会出现一个所谓的叫晋升的操作,那这里的晋升呢,其实指的就是,将新生代的一个对象,移动至我们的老生代进行存储。

那么至于说,什么时候我们能够去,触发这样的一个晋升操作呢?

在这我们一般呢是有两个判断标准的,第一个就是呢,如果我们新生代当中的某些对象经过一轮GC之后呢,他还活着。所以这个时候我们就可以呢把他拷贝至我们的老年代存储区,进行一个存储操作。

除此之外呢,如果说我们当前在拷贝的一个过程中,发现呢这个To空间的使用率超过了25%,那么这个时候我们也需要将这一次的活动对象呢,都移动至老生代中进行存放。

那这个时候我们为什么要选择这样一个25%呢?其实也很容易想得通,因为我们再将来进行回收操作的时候,最终是要把From空间和我们的To空间呢进行一个交换。

也就是说以前的To会变成From,而以前的From呢要变成To。那么这就意味着,我们的To如果使用率达到了80%,那么最终他变成活动对象的一个存储空间后,那么新的对象好像就存不进去了。

这点呢可能有些绕,再简单的说明下就是To空间的使用率如果超过了一定的限制,那么将来他变成使用状态时,那么我们新进来的这样一些对象空间,那么好像就不是那么够用了,所以在这里呢我们会有这样一个限制操作。

那么这一块呢,我们就说完了,在V8当中是如何完成新生代存储区的一个垃圾回收的,简单总结一下就是,当前我们的内存一分为二一部分用来存储新生代对象,至于说什么是新生代对象呢,我们就可以认为他的存活时间呢,相对较短。

然后我们当前就可以去采用标记整理的算法,去对我们当前的From空间进行一个活动对象的标记和整理操作,然后接着去把他们呢拷贝至我们当前的To空间。

最后呢再置换一下两个空间的状态,那此时呢我们也就完成了一个空间的释放操作,那这些就是关于,V8当中如何针对于新生代对象进行一个垃圾回收的操作。

如何回收老生代对象

在这里呢我们来看一下,V8是如何回收老生代对象区域的一些垃圾的,那么这里我们首先呢还是对老生代这样一个空间进行一些说明。

首先,老生代对象呢是存放在内存空间的右侧,针对于老生代区域呢,在V8当中他同样是有一个内存大小的限制,在64位操作系统当中呢这个大小是1.4G, 在32位操作系统当中是700M。

那么有了这样一个大小之后,我们就可以在里面去存放具体的数据了。

老生代对象指的就是当前存活时间较长的对象,例如我们之前所提到的在全局对象下所存放的一些变量。再或者就是一些闭包里面所放置的一些变量数据,有可能也会存活很长的时间。

我们接下来看一下老生代是如何完成垃圾回收的,针对于老生代垃圾回收主要采用的是标记清除,标记整理和增量标记三个算法。

具体使用时他主要采用的是标记清除算法去完成对应的垃圾空间的释放和回收,之前我们已经说过标记清除算法的工作原理,这里我们就不再具体说明。主要就是找到我们老生代存储区域当中的所有活动对象进行标记,然后直接释放掉那些垃圾数据空间,就可以了。

显而易见呢这个地方他就会存在一些空间碎片化上的一些问题,不过虽然有这样一些问题存在,但是在V8的底层主要使用的还是标记清除的算法。因为相对于那些空间碎片来说他的提升速度是非常明显的。

在什么情况下我们会去使用到标记整理算法呢?如果说我们发现,当他需要把我们新生代区域里面的内容向老生代中移动的时候,而且这个时间节点上老生代存储区域的空间又不足以存放我们新生代存储区所移过来的这样一些对象。在这种情况下就会去触发标记整理,把之前的一些锁片空间进行整理回收,这样就让我们有更多的空间可以使用。

那么最后呢,他还会采用增量标记方式对我们当前这样一个回收的效率呢进行提升,这里后面我们会介绍到。

那么现在我们已经知道了V8对于老生代对象是如何处理垃圾回收的,这里我们再来对比一下新老生代垃圾回收。

我们新生代的垃圾回收更像是在用空间换时间,因为他采用的是一个复制算法,那么这样也就意味着每时每刻他的内部都会有一个空闲空间的存在。

但是由于我们新生代存储区他本身的空间就很小,所以分出来的空间就更小,所以这一部分的空间浪费对于他所带来的时间上的一个提升是微不足道的。

在我们老生代对象回收过程中为什么不去采用这种一分二位的做法呢?因为老生代存储空间是比较大的,如果一分为二就有几百兆的空间浪费,这太奢侈了。

第二呢就是我们老生代存储区域中所存放的对象数据比较多,所以在赋值的过程中消耗的时间也就非常对,由此我们就知道老生代的垃圾回收是不适合使用复制算法来实现的。

这里我们对细节的描述也就说完了,那么接下来我们之前所提到的通过增量标记的一个算法去优化垃圾回收操作,这里如何理解呢?

这里我们来说明一下,首先我们分成两个部分,一个是程序的执行,另一个是垃圾回收。

这里我们首先明确垃圾回收进行工作的时候,是会阻塞当前JavaScript程序执行的,所以会出现一个空档期。例如程序支撑完成之后会停下来去执行垃圾回收操作。

所谓的标记增量简单的来说就是将整段的垃圾回收操作拆分成多个小步,组合着去完成整个回收,去替代之前的一口气做完的垃圾回收操作。

这样做的好处主要是实现垃圾回收与程序执行交替完成,这样做带来的时间消耗会更加的合理一些。避免像以前那样程序执行的时候不能做垃圾回收,程序做垃圾回收的时候不能继续运行程序。

简单的举个例子,说明一下增量标记的实现原理。

程序首先运行的时候是不需要进行垃圾回收的,一旦当他触发了垃圾回收之后,那么这里无论我们采用的是何种算法,都会进行遍历和标记的操作,这里要明确,针对的是老生代存储区域,所以是存在遍历操作的。

在遍历的过程中需要做标记,而这个标记之前也提到过,可以不一口气做完,为什么呢?因为他存在一个直接可达和间接可达操作,也就是说如果我们在做的时候,第一步先找到第一层的可达对象。那么我们就可以停下来了,让程序再去执行一会。

如果说程序执行了一会以后,在继续让GC机制去做他的二步的一个标记操作,比如下面还有一些子元素也是可达的,那就继续去把他做一个标记。

标记一轮之后再让GC停下来,继续回到程序执行,也就是交替的去做标记和程序执行。

最后标记操作完成以后,再去完成垃圾回收,这段时间程序就要停下来不执行等到垃圾回收操作完成,程序继续执行。

虽然这样看起来当前的程序停顿了很多次,但是我们要明白,整个V8最大的一个垃圾回收当内存达到1.5G的时候,采用非增量标记的形式去进行垃圾回收,时间也不超过1s,所以这里程序的一个间断是合理的。

而且这样一来他就最大限度的把以前很长的一段停顿时间直接拆分成了更小端,这对于用户来说就会显得更加流程一些。

到底为止就说完了V8当中是如何完成对于老生代存储区中的垃圾回收操作的。

简单整理一下就是有三个算法,编辑清除,标记整理和增量标记。增量标记的方式去可以提高回收效率。

垃圾回收总结

这里我们对之前所提到的关于V8引擎的垃圾回收操作进行一些整理和总结。

首先要知道V8引擎其实是当前主流的一个JavaScript执行引擎,然后在V8的内部他的内存是设置上限的,这么做的原因是因为第一他本身是为浏览器而设置的,所以针对于web应用来说,这样的内存大小是足够使用的。

第二就是由他内部的垃圾回收机制来决定的,如果我们再去把内存设置大一些那这个时候他的回收时间最多可能就超过了用户的感知,所以这里就设置了一个上限数值。

V8采用的是分代回收的思想,将内存分成了新生代和老生代。关于新生代和老生代空间和存储的数据类型是不同的。对于新生代,如果在64位操作系统下大小空间是32M, 如果在32位的系统下就是16M。

V8对于不同代对象采用的是不同的GC算法来完成垃圾回收操作,具体来说就是针对新生代采用复制算法和标记整理算法,针对于老生代对象主要采用标记清除,标记整理和增量标记这样三个算法。

这里我们做了一个简单的梳理,里面还有一些细节性的问题,比如说为什么不在老生代当中去做一些复制操作,比如说什么时候将新生代当中的对象去复制到老生代。具体的内容可以在前面讲解的内容中找到答案,这里我们就说完了。