XSS攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如cookie等。
XSS的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
攻击者可以通过这种攻击方式可以进行以下操作:
XSS可以分为存储型、反射型和DOM型:
1)存储型XSS的攻击步骤:
2)反射型XSS的攻击步骤:
反射型XSS跟存储型XSS的区别是:存储型XSS的恶意代码存在数据库,反射型XSS的恶意代码存在URL。
反射型XSS漏洞常于通过URL传递参数的功能,如站搜索、跳转等。由于需要户主动打开恶意的URL才能效,攻击者往往会结合多种段诱导户点击。
3)DOM型XSS的攻击步骤:
DOM型XSS跟前两种XSS的区别:DOM型XSS攻击中,取出和执恶意代码由浏览器端完成,属于前端JavaScript身的安全漏洞,其他两种XSS都属于服务端的安全漏洞。
可以看到XSS危害如此之大,那么在开发网站时就要做好防御措施,具体措施如下:
CSRF攻击的本质是利用cookie会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
常见的CSRF攻击有三种:
CSRF攻击可以使用以下方法来防护:
中间(Man-in-the-middleattack,MITM)是指攻击者与通讯的两端分别创建独的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过个私密的连接与对直接对话,但事实上整个会话都被攻击者完全控制。在中间攻击中,攻击者可以拦截通讯双的通话并插新的内容。
攻击过程如下:
络劫持分为两种:
(1)DNS劫持:(输京东被强制跳转到淘宝这就属于dns劫持)
进程是资源分配的最小单位,线程是CPU调度的最小单位。
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间。
如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存。
进程和线程之间的关系有以下四个特点:
(1)进程中的任意一线程执行出错,都会导致整个进程的崩溃。
(2)线程之间共享进程中的数据。
(3)当一个进程关闭之后,操作系统会回收进程所占用的内存,**当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
(4)进程之间的内容相互隔离。**进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程A写入数据到进程B的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。
Chrome浏览器的架构图:
从图中可以看出,最新的Chrome浏览器包括:
这些进程的功能:
所以,打开一个网页,最少需要四个进程:1个网络进程、1个浏览器进程、1个GPU进程以及1个渲染进程。如果打开的页面有运行插件的话,还需要再加上1个插件进程。
虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:
浏览器的渲染进程的线程总共有五种:
(1)GUI渲染线程
负责渲染浏览器页面,解析HTML、CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
(2)JS引擎线程
JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;JS引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序;
(3)事件触发线程
事件触发线程属于浏览器而不是JS引擎,用来控制事件循环;当JS引擎执行代码块如setTimeOut时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理;
注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行);
(4)定时器触发进程
(1)管道通信
管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。
管道的特点:
(2)消息队列通信
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
(3)信号量通信
共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是1,然后a进程来访问内存1的时候,我们就把信号量的值设为0,然后进程b也要来访问内存1的时候,看到信号量的值为0就知道已经有进程在访问内存1了,这个时候进程b就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。
(4)信号通信
信号(Signals)是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。
(5)共享内存通信
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
(6)套接字通信
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
系统中的资源可以分为两类:
产生死锁的原因:
(1)竞争资源
(2)进程间推进顺序非法
若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁
产生死锁的必要条件:
预防死锁的方法:
实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通信方法如下:
ServiceWorker是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用ServiceWorker的话,传输协议必须为HTTPS。因为ServiceWorker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。
ServiceWorker实现缓存功能一般分为三个步骤:首先需要先注册ServiceWorker,然后监听到install事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
//index.jsif(navigator.serviceWorker){navigator.serviceWorker.register('sw.js').then(function(registration){console.log('serviceworker注册成功')}).catch(function(err){console.log('servcieworker注册失败')})}//sw.js//监听`install`事件,回调中缓存所需文件self.addEventListener('install',e=>{e.waitUntil(caches.open('my-cache').then(function(cache){returncache.addAll(['./index.html','./index.js'])}))})//拦截所有请求事件//如果缓存中已经有请求的数据就直接用缓存,否则去请求数据self.addEventListener('fetch',e=>{e.respondWith(caches.match(e.request).then(function(response){if(response){returnresponse}console.log('fetchsource')}))})打开页面,可以在开发者工具中的Application看到ServiceWorker已经启动了:
在Cache中也可以发现所需的文件已被缓存:
浏览器缓存的全过程:
很多网站的资源后面都加了版本号,这样做的目的是:每次升级了JS或CSS文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的JS或CSS文件,以保证用户能够及时获得网站的最新更新。
资源缓存的位置一共有3种,按优先级从高到低分别是:
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。
Cache-Control可设置的字段:
一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control的优先级要高于Expires。
no-cache和no-store很容易混淆:
如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。
上面已经说到了,命中协商缓存的条件有两个:
使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个304状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。
当Last-Modified和Etag属性同时出现的时候,Etag的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的Last-Modified应该保持一致,因为每个服务器上Etag的值都不一样,因此在考虑负载平衡时,最好不要设置Etag属性。
总结:
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
对于浏览器的缓存,主要针对的是前端的静态资源,最好的效果就是,在发起请求之后,拉取相应的静态资源,并保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,就直接从本地读取即可,如果服务器的静态资源已经更新,那么我们再次请求的时候,就到服务器拉取新的资源,并保存在本地。这样就大大的减少了请求的次数,提高了网站的性能。这就要用到浏览器的缓存策略了。
所谓的浏览器缓存指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。
使用浏览器缓存,有以下优点:
浏览器的主要功能是将用户选择的web资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是HTML,也包括PDF、image及其他格式。用户用URI(UniformResourceIdentifier统一资源标识符)来指定所请求资源的位置。
HTML和CSS规范中规定了浏览器解释html文档的方式,由W3C组织对这些规范进行维护,W3C是负责制定web标准的组织。但是浏览器厂商纷纷开发自己的扩展,对规范的遵循并不完善,这为web开发者带来了严重的兼容性问题。
浏览器可以分为两部分,shell和内核。其中shell的种类相对比较多,内核则比较少。也有一些浏览器并不区分外壳和内核。从Mozilla将Gecko独立出来后,才有了外壳和内核的明确划分。
浏览器内核主要分成两部分:
最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎。
(1)IE浏览器内核:Trident内核,也是俗称的IE内核;
(2)Chrome浏览器内核:统称为Chromium内核或Chrome内核,以前是Webkit内核,现在是Blink内核;
(3)Firefox浏览器内核:Gecko内核,俗称Firefox内核;
(4)Safari浏览器内核:Webkit内核;
(5)Opera浏览器内核:最初是自己的Presto内核,后来加入谷歌大军,从Webkit又到了Blink内核;
(6)360浏览器、猎豹浏览器内核:IE+Chrome双内核;
(8)百度浏览器、世界之窗内核:IE内核;
(9)2345浏览器内核:好像以前是IE内核,现在也是IE+Chrome双内核了;
(10)UC浏览器内核:这个众口不一,UC说是他们自己研发的U3内核,但好像还是基于Webkit和Trident,还有说是基于火狐内核。
值得注意的是,和多数浏览器不同,Chrome浏览器的每个标签都分别对应个呈现引擎实例。每个标签都是个独的进程。
浏览器渲染主要有以下步骤:
大致过程如图所示:
注意:这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
(1)针对JavaScript:JavaScript既会阻塞HTML的解析,也会阻塞CSS的解析。因此我们可以对JavaScript的加载方式进行改变,来进行优化:
(1)尽量将JavaScript文件放在body的最后
(2)body中间尽量不要写(7)location.hash+iframe跨域实现原理:a欲与b跨域相互通信,通过中间页c来实现。三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html->B域:b.html->A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
(8)window.name+iframe跨域window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的name值(2MB)。
中间代理页,与a.html同域,内容为空即可。
3)b.html:(domain2.com/b.html)
通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
WebSocketprotocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是serverpush技术的一种很好的实现。
原生WebSocketAPI使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
1)前端代码:
服务器为了能够将工作负载分不到多个服务器来提高网站性能(负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。
一般使用反向代理后,需要通过修改DNS让域名解析到代理服务器IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
两者区别如图示:
正向代理和反向代理的结构是一样的,都是client-proxy-server的结构,它们主要的区别就在于中间这个proxy是哪一方设置的。在正向代理中,proxy是client设置的,用来隐藏client;而在反向代理中,proxy是server设置的,用来隐藏server。
Nginx是一款轻量级的Web服务器,也可以用于反向代理、负载平衡和HTTP缓存等。Nginx使用异步事件驱动的方法来处理请求,是一款面向性能设计的HTTP服务器。
传统的Web服务器如Apache是process-based模型的,而Nginx是基于event-driven模型的。正是这个主要的区别带给了Nginx在性能上的优势。
Nginx架构的最顶层是一个masterprocess,这个masterprocess用于产生其他的workerprocess,这一点和Apache非常像,但是Nginx的workerprocess可以同时处理大量的HTTP请求,而每个Apacheprocess只能处理一个。
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:
事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托(事件代理)。
使用事件委托可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理还可以实现事件的动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理。
如果有一个列表,列表之中有大量的列表项,需要在点击列表项的时候响应一个事件:
给上述的例子中每个列表项都绑定事件,在很多时候,需要通过AJAX或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的,所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。
//来实现把#list下的li元素的事件代理委托到它的父层元素也就是#list上://给父层元素绑定事件document.getElementById('list').addEventListener('click',function(e){//兼容性处理varevent=e||window.event;vartarget=event.target||event.srcElement;//判断是否匹配目标元素if(target.nodeName.toLocaleLowerCase==='li'){console.log('thecontentis:',target.innerHTML);}});在上述代码中,target元素则是在#list元素之下具体被点击的元素,然后通过判断target的一些属性(比如:nodeName,id等等)可以更精确地匹配到某一类#listli元素之上;
当然,事件委托也是有局限的。比如focus、blur之类的事件没有事件冒泡机制,所以无法实现事件委托;mousemove、mouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。
当然事件委托不是只有优点,它也是有缺点的,事件委托会影响页面性能,主要影响因素有:
在必须使用事件委托的地方,可以进行如下的处理:
场景:给页面的所有的a标签添加click事件,代码如下:
document.addEventListener("click",function(e){if(e.target.nodeName=="A")console.log("a");},false);但是这些a标签可能包含一些像span、img等元素,如果点击到了这些a标签中的元素,就不会触发click事件,因为事件绑定上在a标签元素上,而触发这些内部的元素时,e.target指向的是触发click事件的元素(span、img等其他元素)。
这种情况下就可以使用事件委托来处理,将事件绑定在a标签的内部元素上,当点击它的时候,就会逐级向上查找,知道找到a标签为止,代码如下:
EventLoop执行顺序如下所示:
可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
当开始执行JS代码时,根据先进后出的原则,后执行的函数会先弹出栈,可以看到,foo函数后执行,当执行完毕后就从栈中弹出了。
平时在开发中,可以在报错中找到执行栈的痕迹:
可以看到报错在foo函数,foo函数又是在bar函数中调用的。当使用递归时,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题
Node中的EventLoop和浏览器中的是完全不相同的东西。
Node的EventLoop分为6个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含setTimeout和setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入Pendingcallbacks阶段。
(3)Idle/Prepare:仅供内部使用。
(4)Poll(轮询阶段):
(6)Closecallbacks:执行一些关闭回调,比如socket.on('close',...)等。
下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机的
setTimeout(()=>{console.log('setTimeout')},0)setImmediate(()=>{console.log('setImmediate')})对于以上代码来说,setTimeout可能执行在前,也可能执行在后
当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:
constfs=require('fs')fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout');},0)setImmediate(()=>{console.log('immediate')})})在上述代码中,setImmediate永远先执行。因为两个代码写在IO回调中,IO回调是在poll阶段执行,当回调执行完毕后队列为空,发现存在setImmediate回调,所以就直接跳转到check阶段去执行回调了。
上面都是macrotask的执行情况,对于microtask来说,它会在以上每个阶段完成前清空microtask队列,下图中的Tick就代表了microtask
setTimeout(()=>{console.log('timer21')},0)Promise.resolve().then(function(){console.log('promise1')})对于以上代码来说,其实和浏览器中的输出是一样的,microtask永远执行在macrotask前面。
最后来看Node中的process.nextTick,这个函数其实是独立于EventLoop之外的,它有一个自己的队列,当每个阶段完成后,如果存在nextTick队列,就会清空队列中的所有回调函数,并且优先于其他microtask执行。
setTimeout(()=>{console.log('timer1')Promise.resolve().then(function(){console.log('promise1')})},0)process.nextTick(()=>{console.log('nextTick')process.nextTick(()=>{console.log('nextTick')process.nextTick(()=>{console.log('nextTick')process.nextTick(()=>{console.log('nextTick')})})})})对于以上代码,永远都是先把nextTick全部打印出来。
顺序
//macro-task:script(全部的代码)setIntervalsetTimeoutsetImmediateI/O//micro-task:process.nextTickPromise10.事件触发的过程是怎样的事件触发有三个阶段:
事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个body中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。
//以下会先打印冒泡然后是捕获node.addEventListener('click',event=>{console.log('冒泡')},false)node.addEventListener('click',event=>{console.log('捕获')},true)通常使用addEventListener注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值useCapture参数来说,该参数默认值为false,useCapture决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性:
一般来说,如果只希望事件只触发在目标上,这时候可以使用stopPropagation来阻止事件的进一步传播。通常认为stopPropagation是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。
stopImmediatePropagation同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
node.addEventListener('click',event=>{event.stopImmediatePropagation()console.log('冒泡')},false)//点击node只会执行上面的函数,该函数不会执行node.addEventListener('click',event=>{console.log('捕获')},true)九、浏览器垃圾回收机制1.V8的垃圾回收机制是怎样的V8实现了准确式GC,GC算法采用了分代式垃圾回收机制。因此,V8将内存(堆)分为新生代和老生代两部分。
(1)新生代算法
在新生代空间中,内存空间分为两部分,分别为From空间和To空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入From空间中,当From空间被占满时,新生代GC就会启动了。算法会检查From空间中存活的对象并复制到To空间中,如果有失活的对象就会销毁。当复制完成后将From空间和To空间互换,这样GC就结束了。
(2)老生代算法
先来说下什么情况下对象会出现在老生代空间中:
老生代中的空间很复杂,有如下几个空间
enumAllocationSpace{//TODO(v8:7464):Actuallymapthisspace'smemoryasread-only.RO_SPACE,//不变的对象空间NEW_SPACE,//新生代用于GC复制算法的空间OLD_SPACE,//老生代常驻对象空间CODE_SPACE,//老生代代码对象空间MAP_SPACE,//老生代map对象LO_SPACE,//老生代大空间对象NEW_LO_SPACE,//新生代大空间对象FIRST_SPACE=RO_SPACE,LAST_SPACE=NEW_LO_SPACE,FIRST_GROWABLE_PAGED_SPACE=OLD_SPACE,LAST_GROWABLE_PAGED_SPACE=MAP_SPACE};在老生代中,以下情况会先启动标记清除算法:
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011年,V8从stop-the-world标记切换到增量标志。在增量标记期间,GC将标记工作分解为更小的模块,可以让JS应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在2018年,GC技术又有了一个重大突破,这项技术名为并发标记。该技术可以让GC扫描和标记对象时,同时允许JS运行。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。