前端八股之浏览器原理
1044
浏览器安全
1. 什么是XSS攻击?
概念
XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
攻击者可以通过这种攻击方式可以进行以下操作:
- 获取页面的数据,如DOM、cookie、localStorage
- DOS攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器
- 破坏页面结构
- 流量劫持(将链接指向某网站)
攻击类型
XSS 可以分为存储型、反射型和DOM型:
- 存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行。
- 反射型指的是攻击者诱导用户访问一个带有恶意代码的URL后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有XSS代码的数据后当做脚本执行
- DOM型指的通过修改页面的DOM节点形成的XSS
2. 如何防御XSS攻击?
可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到 HTML 中的代码做好充分的转义。
使用CSP ,CSP的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。
CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
通常有两种方式来开启 CSP,一种是设置HTTP首部中的Content-Security-Policy
,一种是设置meta标签的方式<meta http-equiv="Content-Security-Policy">
对一些敏感信息进行保护,比如cookie使用http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。
3. 什么是CSRF攻击?
概念
CSRF攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。
CSRF攻击的本质是利用cookie会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
攻击类型
常见的CSRF攻击有三种:
- GET类型的CSRF攻击,比如在网站中的一个im 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
- POST类型的CSRF攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
- 链接类型的CSRF攻击,比如在a标签的href属性里构建一个请求,然后诱导用户去点击。
4. 如何防御CSRF攻击?
- 进行同源检测,服务器根据http请求头中origin或者referer信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当origin或者referer信息都不存在的时候,直接阻止请求。
- 使用CSRF Token进行验证,服务器向用户返回一个随机数Token,当网站再次发起请求时,在请求参数中加入服务器端返回的token,然后服务器对这个token进行验证。
- 对Cookie进行双重验证,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从cookie中取出这个字符串,添加到URL参数中,然后服务器通过对cookie中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用cookie,但是不能访问获取cookie的特点。
- 在设置cookie属性的时候设置Samesite,限制cookie不能作为被第三方使用,从而可以避免被攻击者利用。
浏览器进程与线程
1. 如何实现浏览器内多个标签页之间的通信?
实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。
- 使用websocket协议,因为websocket协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。
- 使用localStorage的方式,我们可以在一个标签页对localStorage的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。
- 使用postMessage方法,如果我们能够获得对应标签页的引用,就可以使用postMessage方法,进行通信。
2. 对Service Worker的理解
Service Worker是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用Service Worker的话,传输协议必须为HTTPS。因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。
Service Worker实现缓存功能一般分为三个步骤:首先需要先注册Service Worker,然后监听到install事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
// index.js
if (!navigator.serviceWorker) {
return
}
navigator.serviceWorker
.register('sw.js')
.then((registration) => {
console.log('注册成功')
})
.catch((err) => {
console.log('注册失败')
})
// sw.js
// 监听`install`实践,回调中缓存所需文件
self.addEventListener('install', (event) => {
e.waitUntil(
caches.open('my-cache').then((cache) => cache.addAll(['index.html', 'index.js']))
)
})
// 拦截所有请求实践
// 如果缓存中已经有请求的数据就直接用缓存,否则就去请求数据
self.addEventListener('fetch', (event) => {
e.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response
}
console.log('请求数据')
})
)
})
浏览器缓存
1. 对浏览器的缓存机制的理解
浏览器缓存的全过程:
- 浏览器第一次加载资源,服务器返回200,浏览器从服务器下载资源文件,并缓存资源文件,以供下次加载时对比使用
- 下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上一次返回200时的时间差,如果没有超过
cache-control
设置的max-age
,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持HTTP1.1,则使用expires
头判断是否过期 - 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有
If-None-Match
和If-Modified-Since
的请求 - 服务器收到请求后,优先根据
Etag
的值判断被请求的文件有没有做修改,Etag
值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag
值并返回200
- 如果服务器收到的请求没有
Etag
值,则将If-Modified-Since
和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的Last-Modified
和文件并返回200
2. 浏览器资源缓存的位置有哪些?
资源缓存的位置一共有3种,按优先级从高到低分别是:
- Service Worker:Service Worker运行在JavaScript主线程之外,虽然由于脱离了浏览器窗体无法直接访问DOM,但是它可以完成离线缓存、消息推送、网络代理等功能。它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
当Service Worker没有命中缓存的时候,需要去调用fetch函数获取 数据。也就是说,如果没有在Service Worker命中缓存,会根据缓存查找优先级去查找数据。但是不管是从Memory Cache中还是从网络请求中获取的数据,浏览器都会显示是从Service Worker中获取的内容。 - Memory Cache:Memory Cache就是内存缓存,它的效率最快,但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭Tab页面,内存中的缓存也就被释放了。
- Disk Cache:Disk Cache也就是存储在硬盘中的缓存。在所有浏览器缓存中,Disk Cache覆盖面基本是最大的。它会根据HTTP Herder中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
3. 点击刷新按钮或者按 F5、按 Ctrl+F5(强制刷新)、地址栏回车有什么区别?
点击刷新按钮或者按F5:浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since
,If-None-Match
,这就意味着服务器会对文件检查新鲜度,返回结果可能是304,也有可能是200。
用户按Ctrl+F5(强制刷新):浏览器不仅会对本地文件过期,而且不会带上If-Modifed-Since
,If-None-Match
,相当于之前从来没有请求过,返回结果是 200。
地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。
浏览器本地存储
1. 浏览器本地存储方式及使用场景
Cookie
Cookie是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie就出现了。Cookie的大小只有4kb,它是一种纯文本文件,每次发起HTTP请求都会携带Cookie。
Cookie的特性:
- Cookie一旦创建成功,名称就无法修改
- Cookie是无法跨域名的,也就是说a域名和b域名下的cookie是无法共享的,这也是由Cookie的隐私安全性决定的,这样就能够阻止非法获取其他网站的Cookie
- 有安全问题,如果Cookie被拦截了,那就可获得session的所有信息,即使加密也于事无补,无需知道cookie的意义,只要转发cookie就能达到目的
- Cookie在请求一个新的页面的时候都会被发送过去
LocalStorage
LocalStorage是HTML5新引入的特性,由于有的时候我们存储的信息较大,Cookie就不能满足我们的需求,这时候LocalStorage就派上用场了。
LocalStorage的优点:
- 在大小方面,LocalStorage的大小一般为5MB,可以储存更多的信息
- LocalStorage是持久储存,并不会随着页面的关闭而消失,除非主动清理,不然会永久存在
- 仅储存在本地,不像Cookie那样每次HTTP请求都会被携带
LocalStorage的缺点:
- 存在浏览器兼容问题,IE8以下版本的浏览器不支持
- 如果浏览器设置为隐私模式,那我们将无法读取到LocalStorage
- LocalStorage受到同源策略的限制
SessionStorage
SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。
IndexedDB
IndexedDB是浏览器提供的一种底层API,用于客户端存储大量结构化数据(如文件、Blob 等)。与LocalStorage不同,IndexedDB支持事务、索引查询和高性能存取,适合存储复杂或大量数据。
IndexedDB 的特性:
- 非关系型数据库:以键值对形式存储数据,支持复杂对象(如 JSON、文件)。
- 事务支持:所有操作在事务中完成,保证数据操作的原子性。
- 存储容量大:一般可达浏览器剩余磁盘空间的50%(不同浏览器有差异)。
- 同源策略:受同源限制,不同域名无法互相访问。
IndexedDB 的缺点:
- API复杂:需要手动管理数据库版本、对象仓库、索引等
- 兼容性问题:旧版本浏览器(如IE 10以下)不支持。
使用场景:
- 需要离线访问的大型应用(如在线文档、邮件客户端)。
- 存储用户生成的文件(如图片、音频)。
2. Cookie有哪些字段,作用分别是什么
- Name:cookie的名称
- Value:cookie的值
- Size:cookie的大小
- Path:可以访问此cookie的页面路径。比如path是/test,那么只有/test路径下的页面可以读取此cookie。
- Secure:指定是否使用HTTPS安全协议发送Cookie。使用HTTPS安全协议,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。
- Domain:可以访问该cookie的域名,Cookie机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的Cookie。
- HTTP:该字段包含HTTPOnly 属性,该属性用来设置cookie能否通过脚本来访问,默认为空,即可以通过脚本访问
- Expires/Max-size: 此cookie的超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。
浏览器同源策略
1. 什么是同源策略
跨域问题其实就是浏览器的同源策略造成的。
同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议、端口号、域名必须一致。
同源政策主要限制了三个方面:
- 当前域下的js脚本不能够访问其他域下的cookie、localStorage和indexedDB。
- 当前域下的js脚本不能够操作访问操作其他域下的DOM。
- 当前域下ajax无法发送跨域请求。
同源政策的目的主要是为了保证用户的信息安全,它只是对js脚本的一种限制,并不是对浏览器的限制,对于一般的img、或者script脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。
2. 如何解决跨越问题
CORS
下面是MDN对于CORS的定义:
跨域资源共享(CORS) 是一种机制,它使用额外的HTTP头来告诉浏览器让运行在一个origin(domain)上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域HTTP请求。 CORS需要浏览器和服务器同时支持,整个CORS过程都是浏览器完成的,无需用户参与。因此实现CORS的关键就是服务器,只要服务器实现了CORS请求,就可以跨源通信了。
浏览器将CORS分为简单请求和非简单请求:
简单请求不会触发CORS预检请求。若该请求满足以下两个条件,就可以看作是简单请求:
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
: 只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
简单请求过程:
对于简单请求,浏览器会直接发出CORS请求,它会在请求的头信息中增加一个Origin
字段,该字段用来说明本次请求来自哪个源(协议+端口+域名),服务器会根据这个值来决定是否同意这次请求。如果Origin
指定的域名在许可范围之内,服务器返回的响应就会多出以下信息头
Access-Control-Allow-Origin: http://xxx.xxx.com // 和Orign一致
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Expose-Headers: FooBar // 指定返回其他字段的值
Content-Type: text/html; charset=utf-8 // 表示文档类型
在简单请求中,在服务器内,至少需要设置字段:Access-Control-Allow-Origin
非简单请求过程:
非简单请求是对服务器有特殊要求的请求,比如请求方法为DELETE或者PUT等。非简单请求的CORS请求会在正式通信之前进行一次HTTP查询请求,称为预检请求。
预检请求使用的请求方法是OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是Orign,表示请求来自哪个源。除此之外,头信息中还包括两个字段:
- Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法。
- Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。
服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有
Access-Control-Allow-Origin
这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。
在非简单请求中,至少需要设置以下字段:'Access-Control-Allow-Origin'、 'Access-Control-Allow-Methods' 、'Access-Control-Allow-Headers'
JSONP
jsonp的原理就是利用<script>
标签没有跨域限制,通过<script>
标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。
原生JS实现:
<script>
const script = document.createElement('script')
script.type = 'text/javascript'
// 传参一个回调函数名给后端
script.src = 'http://www.domain.com:8080/login?user=admin&callback=handleCallback'
document.body.appendChild(script)
function handleCallback(res) {
console.log(JSON.stringify(res))
}
</script>
服务端返回如下(返回时即执行全局函数):
handleCallback({"success": true, "user": "admin"})
nginx代理跨域
nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin
…等字段。
nginx配置解决iconfont跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
nginx反向代理接口跨域
实现思路:通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; # 反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; # 修改cookie中域名
index index.html index.htm;
add_header Access-Control-Allow-Origin http://www.domain1.com;
add_header Access-Control-Allow-Credentials true;
}
}
3. 正向代理和反向代理的区别
正向代理
客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。比如梯子。
反向代理
服务器为了能够将工作负载分不到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。
一般使用反向代理后,需要通过修改DNS让域名解析到代理服务器IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
浏览器事件机制
1. 事件是什么?事件模型?
事件是用户操作网页时发生的交互动作,比如click/move
,事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个event
对象,包含了该事件发生时的所有相关信息(event
的属性)以及可以对事件进行的操作(event
的方法)。
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:
- DOM0级事件模型,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过js属性来指定监听函数。所有浏览器都兼容这种方式。直接在dom对象上注册事件名称,就是DOM0写法。
- IE事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过
attachEvent
来添加监听函数,可以添加多个监听函数,会按顺序依次执行。 - DOM2级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从
document
一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和IE事件模型的两个阶段相同。这种事件模型,事件绑定的函数是addEventListener
,其中第三个参数可以指定事件是否在捕获阶段执行。
2. 如何阻止事件冒泡
在事件处理函数中,调用event.stopPropagation()
方法,可以阻止事件冒泡。
3. 对事件委托的理解
事件委托的概念
事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托(事件代理)。
使用事件委托可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理还可以实现事件的动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理。
事件委托的特点
- 减少内存消耗
如果有一个列表,列表之中有大量的列表项,需要在点击列表项的时候响应一个事件:
如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是<ul id="list"> <li>列表项1</li> <li>列表项2</li> <li>列表项3</li> ...... <li>列表项100</li> </ul>
ul
上,然后在执行事件时再去匹配判断目标元素,所以事件委托可以减少大量的内存消耗,节约效率。 - 动态绑定事件
给上述的例子中每个列表项都绑定事件,在很多时候,需要通过AJAX或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系document.querySelector('#list').addEventListener('click', (e) => { // 兼容性处理 const event = e || window.event const target = event.target || event.srcElement // 判断是否匹配目标元素 if (target.nodeName.toLowerCase() === 'li') { console.log('内容是: ', target.innerHTML) } })
局限性
当然,事件委托也是有局限的。比如focus
、blur
之类的事件没有事件冒泡机制,所以无法实现事件委托;mousemove
、mouseout
这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。
4. 同步和异步的区别
同步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等待下去,直到消息返回为止再继续向下执行。
异步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,这个时候进程会继续往下执行,不会阻塞等待消息的返回,当消息返回时系统再通知进程进行处理。
5. 对事件循环的理解
因为js是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。
6. 宏任务和微任务分别有哪些
微任务包括: promise
的回调、node中的process.nextTick
、对Dom变化监听的MutationObserver
。
宏任务包括: script脚本的执行、setTimeout
,setInterval
,setImmediate
一类的定时事件,还有如I/O操作、UI渲染等。
浏览器垃圾回收机制
V8的垃圾回收机制是怎样的
V8实现了准确GC,GC算法采用了分代式垃圾回收机制。因此,V8将内存(堆)分为新生代和老生代两部分。
新生代算法
新生代中的对象一般存活时间较短,使用Scavenge GC算法。
在新生代空间中,内存空间分为两部分,分别为From空间和To空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入From空间中,当From空间被占满时,新生代GC就会启动了。算法会检查From空间中存活的对象并复制到To空间中,如果有失活的对象就会销毁。当复制完成后将From空间和To空间互换,这样GC就结束了。
老生代算法
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
先来说下什么情况下对象会出现在老生代空间中:
- 新生代中的对象是否已经经历过一次Scavenge算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
- To空间的对象占比大小超过25%。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
在老生代中,以下情况会先启动标记清除算法:
- 某一个空间没有分块的时候
- 空间中被对象超过一定限制
- 空间不能保证新生代中的对象移动到老生代中
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8从stop-the-world标记切换到增量标志。在增量标记期间,GC将标记工作分解为更小的模块,可以让JS应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在2018年,GC技术又有了一个重大突破,这项技术名为并发标记。该技术可以让GC扫描和标记对象时,同时允许JS运行。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。