你应该掌握的浏览器相关知识

359 阅读27分钟

这是我参与8月更文挑战的第9天,活动详情查看: 8月更文挑战

一.浏览器安全

1. XSS攻击

跨站脚本攻击 cross-site-script,通过脚本注入的方XSS式进行攻击。攻击者通过在网站中注入恶意 的脚本,使其可以在用户的网页中运行,从而可以修改用户的DOM或者拿到用户的cookie。

xss攻击的本质是网页没有对输入的内容进行过滤,使其于正常的代码正常运行。从而导致了一些恶意脚本的运行。

攻击者可以通过xss攻击对用户的页面做出下面操作:

  1. 获取页面的数据,cookie、localStorage、DOM等
  2. 破坏页面结构
  3. 流量劫持(将链接指向某网站)

攻击类型

  1. 存储型:指恶意的脚本存储在服务端,当客户端请回恶意代码后会执行
  2. 反射型:诱导用户访问带有恶意代码的超链接,服务器返回带有恶意代码的结果集,执行
  3. DOM型:通过修改页面的DOM节点形成的xss

所以我们可以得出一个结论,前两种类型都是需要在服务端处理的,DOM型是前端自身的安全漏洞,需要在前端进行处理。

防御措施

  • 输入过滤,对一些DOM操作的内容进行转义,比如出现标签这类的关键代码时
  • 输出过滤,设置csp白名单告诉浏览器应该加载哪些代码,从而使浏览器拦截恶意代码,
  • 对一些敏感信息进行保护,比如cookie使用http-only,使脚本无法获得。
  • 也可以使用验证码避免脚本伪装成用户执行一些操作

2. CSRF攻击

跨站请求伪造攻击(Cross-state-request-forgery),是诱导用户进入一个自己的第三方网站,然后该网站向被攻击的网站发送一些跨站请求。如果用户此时是一个登陆状态,那么攻击者就可以利用这个登陆状态,直接冒充用户向服务器进行操作。

预防手段

  • 同源检查:服务器根据http请求头中的refer信息进行判断请求是否为允许访问的站点,从而对请求进行过滤。但是问题是refer可能会被伪造(不安全的)
  • 使用token:服务端向用户返回一个随机数token,当网站再次发起请求时,在请求参数中加入服务端返回的token,然后服务端对这个token进行验证。

3.有哪些可能引起的前端安全的问题

  • XSS脚本注入攻击
  • iframe的滥用
  • csrf跨站请求伪造
  • 恶意的第三方库

二.浏览器缓存机制

1.对缓存的理解

浏览器的缓存主要针对的时前端的静态资源,在发起请求后,拉取到服务器上的资源并且保存在本地内存或者磁盘中。在下一次发送同样的请求时就可以直接在本地拿取资源。如果服务器上的资源已经更新我们就再次去请求资源并且保存在本地。这样做的好处是大大减少了请求的次数,提高了网站的性能。

使用浏览器的缓存主要有以下的优点:

  1. 加快了客户端加载资源的速度
  2. 减轻了服务器的压力
  3. 减少了多次的网络传输

2.浏览器缓存的分类

强缓存

如果缓存资源有效,直接从本地缓存中获取数据,不必发起请求。

强缓存策略可以通过两种方式来设置,分别是http响应头信息中的Expires属性和Cache-Control属性

需要注意的是Expires属性是http1.0中的属性,它指定的是强缓存的有效日期,有时会和客户端的时间有偏差。所以http1.1引出了cache-control的属性,这属性是一个有效时期,类似于保质期之类的,可以对客户端缓存做出更加精确的控制。

一般而言的话只设置其中即可,要是两者同时存在的话Cache-Control 的优先级要高于 Expires。

协商缓存

向服务器发送请求,服务器会根据请求头的资源判断是否命中协商缓存。如果命中,服务器会返回304的状态码通知浏览器从缓存中读取资源

协商缓存也有常见的两种方式来判断是否缓存

服务器返回的Last-Modified和客户端请求时的if-modified-since

服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。

缺点:这两个的属性值时一个是时间值,只能精确到秒,但是如果我们服务器的时间单位是毫秒时就会出现问题。

所以就引出了:Etag和 If-None-Match 属性

这两个属性和上面的工作流程相似,只是属性值不同,这个属性值时一个唯一标识符。不会出现时间混乱的情况。

注:Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高

总结:

强缓存和协商缓存命中后都会拿取本地缓存的资源,区别就是协商缓存会向服务器发送一次请求。它们缓存不命中时都会正常请求。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。

3.浏览器的缓存机制的全过程

  1. 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时对比使用;
  2. 第二次访问时,先进行强缓存,于我们的expires的值和我们当前的值对比(如果设置了cache-control则使用cache-contral中的值进行对比),如果命中,则直接在缓存中读取,
  3. 如果资源已过期,则开始协商缓存,向服务器发送带有if-modified-since或者if-none-Match的请求,判断服务器资源是否更改
  4. 服务器收到请求后,优先使用etag的值来根据if-none-match的值判断资源是否已修改,如果未修改则命中协商缓存,返回304状态码;如果不一致直接返回请求的资源,状态码为200(如果没有etag的值就是用last-modified的值)

业务流程图1.png

三.浏览器渲染原理

1.浏览器渲染引擎的主要模块以及渲染过程

一个渲染引擎主要包括:HTML解析器,CSS解析器,JS引擎,布局Layout模块,绘图模块等。

HTML解析器:主要用来解析HTML文档,将HTML中的元素组织为DOM树

CSS解析器:为DOM中的各个元素对象计算出样式信息

Javascript引擎:使用js代码可以修改网页的内容,以及更改样式,JavaScript引擎能够解释JavaScript代码

布局模块:DOM创建之后,需要将其中的元素对象同样式信息结合起来,计算它们的大小位置等布局信息,对元素进行布局

绘图模块:将布局计算后的各个网页节点绘制成图像结果

浏览器的大致渲染过程:

1.首先解析HTML标记,调用HTML解析器解析的对应的token(一个token就是一个标签文本的序列化)并且构建DOM树(一块内存保存着我们解析出来的tokens并建立联系)

2.遇到link标记调用相应解析器处理CSS标记,并且构建出CSS样式树

3.遇见script,调用JavaScript引擎处理script标记、绑定事件、修改DOM树/CSS树等

4.将DOM树与CSS树合并成一个渲染树

5.根据渲染树来渲染,以计算每个节点的几何信息(这一过程需要依赖GPU)

6.最终将每个节点绘制到屏幕上

img

3. style样式渲染

首先style标签中的样式由HTML解析器进行解析,页面style标签写的内部样式使异步解析的。浏览器加载资源也是异步的。

style标签样式容易出现闪屏现象,这种现象的解释就是当我们html解析器异步解析html结构和style表中的样式时,html的结构的解析先于样式,这时我们的html结构有了但是部分样式却没有出来,这就会导致我们闪屏现象的产生。

因为我们再开发过程中也要尽量的避免使用style标签

4. link样式渲染

link进来的样式,是由CSS解析器进行解析的,并且CSS解析器在解析样式的过程中会阻塞当前的浏览器渲染过程,也就是说它是同步的。

既然它可以阻塞当前的页面渲染,那么它就可以避免闪屏现象,因为我们最多只是等一会加载的过程,我们完全可以给其加一个loading。这样就可以给用户带来相对较好的用户体验,所以我们推荐使用。

5.阻塞渲染

关于CSS的阻塞

只有link引入的css才能够产生阻塞。因为它使用了CSS解析器进行解析。style里的样式使用的事HTML解析器进行解析

style标签中样式

  • 由HTML解析器进行解析
  • 不阻塞浏览器的渲染(因此会出现“闪屏现象”,结构先于样式的渲染)
  • 不阻塞样式的解析(既然不阻塞渲染,那当然毋庸置疑也就不会阻塞解析了)

link引入的外部css样式:推荐使用的方式

  • 由CSS解析器解析
  • 阻塞浏览器的渲染(因此可以防止“闪屏”现象的发生)
  • 阻塞后面的JS代码执行(JS有操作样式的功能,避免发生冲突所以阻塞,如果不阻塞那么我们不知道最后的样式生效到底是因为css里的操作还是js里的操作)
  • 不阻塞DOM的解析(解析完后不代表可以渲染)

优化css的核心概念:尽可能快的提高外部css加载速度

  • 使用CDN节点进行外部资源的加速
  • 对css进行压缩(利用打包工具,比如webpack;gulp等)
  • 减少http请求数,将多个css进行合并
  • 优化样式表的代码

关于js的阻塞

  • 阻塞后续dom的解析:浏览器不知道后续脚本的内容,如果我们去解析了DOM,但是后面我的js里有对DOM的操作,比如删除了某些DOM,那么浏览器对下面某些DOM的解析就成为了无用功,浏览器无法预估脚本里面具体做了什么。那索性我们就直接阻塞页面的解析
  • 阻塞页面的渲染:其实和上面的解释差不多,还是浏览器担心做一下无用功,js可以操作DOM
  • 阻塞后续js的执行:上下依赖关系

备注:

  1. css的解析和hs的解析是互斥的,css解析的时候js停止解析,js解析的时候css停止解析
  2. 无论css阻塞还是js阻塞,都不会阻塞浏览器引用外部资源(图片、样式、脚本等),因为这样是浏览器加载文档的一种模式,只要是设计网络请求的内容,无论是图片、样式、脚本都会先发送请求去获取资源,至于资源到本地以后浏览器再去协调什么时候使用。
  3. 浏览器的预解析优化:当下的主流浏览器都有这一功能,就是在执行js代码时,浏览器会再去开一个线程去快速解析文档的其余部分,如果后续的操作有网络请求,则发请求;如果后面的代码没有操作DOM的操作,那就打开阻塞,让浏览器去解析DOM(这时和js阻塞相悖的,但是也是现代浏览器的一种优化方案)

四.浏览器本地存储

1. sessionStorage,LocalStorage和Cookie

Cookie

由于Http面向无连接,因此服务器无法发判断网络中的两个请求是否是通过个用户所发送的,为解决这个问题,所以提出了Cookie。Cookie的大小只有4kb,它是一种纯文本文件,每次发送HTTP请求都会携带Cookie

使用场景:

  • 与session结合使用,我们将服务端生成的sessionid存储到Cookie中,每次发请求都会携带这个sessionid,这样服务器就知道是谁发起的请求,从而响应相应的信息。
  • 可以用来统计页面的点击次数

LocalStorage

WebStorage,一般存储5mb的内容,是持久存储,不会随着浏览器的关闭而删除。

特点:

  • 如果浏览器设置为隐私模式,无法读取
  • LocalStorage受到同源策略的显示,如果不再同一个域下不会访问
  • 体积大,持久化

api和sessionStorage相似

使用场景:

  1. 音乐网站游客模式下的播放列表
  2. 游客模式下搜索框访问记录等
  3. 跨页签通信

SessionStorage

SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。

域LocalStorage的对比:

  • 都是本地存储,且存储的体积相似
  • SessionStorage也有同源策略的限制,但是SessionStorage有一条更加严格的限制,SessionStorage只有在同一浏览器的同一窗口下才能够共享
  • LocalStorage和SessionStorage都不能被爬虫爬取

使用场景:

  • 游客登录时网站临时存放的一些信息

2.浏览器的跨页签通信

我们使用LocalStorage进实现跨页前通信,跨页前通信的使用场景就是在我们a页面数据变动后,在不刷新页面的情况下我们的b页面的响应数据也可以发生改变。比如我们的外卖。

我们在a页面中操作input框,在失焦时将其存入到LocalStorage中。

  let a= document.getElementById('tt')
  a.onblur=() => {
    localStorage.setItem('ha',a.value)
  }

在b页面监听storage事件,设置值

 let a=document.getElementById('tt')
  window.addEventListener('storage',(e) => {
    a.value=e.newValue
  })  

五.浏览器的同源策略

1.什么是同源策略

同源策略是浏览器的一个安全机制,它限制了从同一个源的文档向另外一个源进行交互。

同源指的是:协议、端口号、域名必须相同。

我们可以看一下以下的栗子:store.company.com/dir/page.ht…

URL是否跨域原因
store.company.com/dir/page.ht…同源完全相同
store.company.com/dir/inner/a…同源只有路径不同
store.company.com/secure.html跨域协议不同
store.company.com:81/dir/etc.htm…跨域端口不同 ( http:// 默认端口是80)
news.company.com/dir/other.h…跨域主机不同

同源策略只要限制了三个方面:

  1. 限制的不同域的本地存储,比如:cookie、WebStorage、IndexDB
  2. 当前域下的js不能访问不同域下的DOM
  3. 当前域下无法使用Ajax发送跨域请求

同源策略实际上只是对js脚本的一种限制,并不是浏览器的限制,对于一般的img、或者其他资源请求不会发生跨域的限制。

2.如何解决跨域问题

解决跨域的方式其实有七种,分别是jsonp、cors、Proxy代理、postmessage、socket.io、iframe、nginx反向代理。

其中最常用的是前三种:jsonp、cors、Proxy代理跨域

JSONP

我们前面说过同源策略其实是为了限制js脚本的一些操作,而对于文档中其他的比如img、script标签的src引入外部资源的get请求是不会收到跨域问题的影响的。所以我们据可以使用回调函数的方式来进行于服务器的交互。

发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

看一个案例:

server.js

const http = require('http');
const urly = require('url');
var obj={
    name:'jam',
    age:'12'
}
http.createServer((req,res) => {
    var parmer=urly.parse(req.url,true)

    console.log(parmer);
    //判断是否跨域(是否有parmer.query.callback这个值,如果有,拼接成带有参数的函数,传给客户端)
    if (parmer.query.callback) {
        var str=parmer.query.callback+'('+JSON.stringify(obj)+')'
        res.write(str);
    }else{
        res.write(JSON.stringify(obj));
    }
    res.end();
}).listen(8000,function () { 
    console.log('服务器开启');
 });

client.js

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script>
    //要执行的回调函数
    function hello(data) {
        console.log(data);//打印obj
    }
</script>
  //使用script标签进行get请求,后接要传给服务端的回调函数
<script src="http://127.0.0.1:8000/?callback=hello"></script>
<body>
</body>
</html>

jsonp的不足之处:

  1. 只能进行get请求
  2. 不安全,可能会会遭受XSS攻击

CORS

跨域资源共享

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信 没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附 加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

CORS需要浏览器和服务器同时支持,整个CORS过程都是浏览器完成的,无需用户参与。因此实现CORS的关键就是服务器,只要服务器实现了CORS请求,就可以跨源通信了。

我们一般可以设置请求头来规定我们允许访问的源和请求的方式

res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Authorization,X-API-KEY, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method' )
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PATCH, PUT, DELETE')

我们这里需要注意的是在设置Origin字段时可以有两种情况:*和一个域名

在使用*(接受所有域名)的时候浏览器为了安全考虑是不会发送Cookie值的,使用一个域名的时候是可以的。但是在实际开发中我们通常只需要使用一个域名就可以完成业务了。

Proxy代理

通过代理服务器实现数据的转发

字面意思就是类似中间商,开启代理,原理就是在本地创建一个虚拟服务器,发送请求数据,同时接受请求的数据,

利用服务器与服务器间,交互,不会有跨域问题,也是完全只靠前端自己独立解决跨域的方式

vueconfig下配置:

module.exports={
  devServer:{
    host:'localhost',
    port:8080,
    proxy:{
      '/api':{
        target:'http://mall-pre.springboot.cn',
        changeOrigin:true,
        pathRewrite:{
          '/api':''
        }
      }  
    }
  }
}

react的可以在package.json中配置单向跨域代理

	"proxy":{
   
		"/api":{
      
		"target":"http://m.kugo.com",
      				"changeOrigin": true
 ,
		"pathRewrite": {
      			'^/api': '',
    				},  
	}
}

nginx代理跨域

nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin…等字段。

六、浏览器的事件机制

1.事件是什么?事件模型是什么?

事件就是操作网页时发生的交互动作,比如move/click等。

现代浏览器一般有三种事件模型:

DOM0级事件模型

这种模型不会传播,所以没有事件流的概念,直接在dom对象上注册事件名称,就是DOM0写法。

IE事件模型

在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。这种模型通过attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。

DOM2级事件模型

在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。

2.如何阻止事件冒泡

普通浏览器器:event.stopPropagation()

IE浏览器使用:event.cancelBubble = true;

3.对事件委托机制的理解

事件委托机制实际上就是利用了DOM2级事件事件冒泡的机制,子元素将事件委托给父元素进行管理,因为事件再冒泡的过程中会上传给父节点,父元素可以通过事件对象获取到目标节点,因此就可以把对子元素的监听放在父元素上,有父元素统一处理子元素的操作。

使用事件委托可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理还可以实现事件的动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理。

特点:

  • 减少内存的消耗,不需要添加大量的监听事件
  • 动态绑定事件

缺点:

  • dom层级过深时会影响到冒泡的性能
  • 有一定的局限性,比如mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高

优化方案

  • 只在必须的地方使用事件委托
  • 减少绑定的层级,不再body元素上绑定
  • 减少绑定的次数,如果可以,那么把多个事件的绑定,合并到一次事件委托中去,由这个事件委托的回调,来进行分发。

使用案例,给ui标签下的li标签进行统一操作。

 ul.addEventListener('click', function (e) {
        var e = e || window.event;
        var target = e.target || e.srcElement;
        target.innerHTML += 'bbb';
        target.style.color = 'yellow';
    }, false);

4.浏览器事件循环

因为js是单线程的,在代码执行时,通过将不同的函数的执行上下文压入到执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js引擎并不会等待耗时操作执行完毕再去执行下面的同步代码,而是将这些耗时操作挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列分为宏任务队列和微任务队列,当当前的执行栈中的代码执行完以后,js引擎会去判断微任务队列中时候有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列执行完成后再去执行宏任务队列中的任务。(注意在执行宏任务时微任务队列中不能有任务)

img

事件循环的执行顺序如下所示:

  • 首先执行同步代码,这属于宏任务

  • 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行

  • 执行所有微任务

  • 当执行完所有微任务后,如有必要会渲染页面

  • 然后开始下一轮 Event Loop,执行宏任务中的异步代码

5.宏任务和微任务分别有哪些?

**微任务:**promise中then的回调,node中process.nextTick等

**宏任务:**script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件

6.Node中的事件循环和浏览器的事件循环的区别

Node的事件循环分为6个阶段,它们会按照顺序反复执行。每当进入某一个 阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入到下一个阶段。

  • 定时器(Timers):本阶段执行已经被setTimeout()和setInterval()的调度回调函数。
  • 待定回调(Pending Callvack):对某些系统操作执行回调,比如TCP连接时接受到ECONNREFUSED。
  • idle, prepare:仅系统内部使用。
  • 轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调;
  • 检测setImmediate() 回调函数在这里执行
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)

我们发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:

  • 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
  • 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;

但是,Node中的事件循环不只是 微任务队列宏任务队列

  • 微任务队列:
    • next tick queue:process.nextTick;
    • other queue:Promise的then回调、queueMicrotask;
  • 宏任务队列:
    • timer queue:setTimeout、setInterval;
    • poll queue:IO事件;
    • check queue:setImmediate;
    • close queue:close事件;

所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:

  • next tick microtask queue
  • other microtask queue
  • timer queue
  • poll queue
  • check queue
  • close queue

相关面试题:

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout0')
}, 0)

setTimeout(function () {
  console.log('setTimeout2')
}, 300)

setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
  console.log('promise1')
  resolve();
  console.log('promise2')
}).then(function () {
  console.log('promise3')
})

console.log('script end')

输出结果:

script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2

七. 浏览器垃圾回收机制

1.v8垃圾回收机制

V8实现了准确式的GC,V8将堆分为新生代和老生代两部分。

新生代算法

新生代中的对象一般存活时间较短,使用了 Scavenge GC 算法。

在新生代的算法空间中一般内存空间分为两部分,一个是form,一个是to。在这两个空间中,必定有一个空间是正在使用的,另一个空间是空闲的。新分配的空间被分配到from空间中去,当from空间被占满时,新生代空间就会执行。算法会将存活的对象复制到To空间中,将失活的对象清除。复制完成后再将当前的To空间与from空间交换,这样就算一轮GC结束

老生代算法

新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。

因为老生代中管理着大量的存活对象,如果依旧使用新生代中的复制交换算法的话,会明显的浪费大量空间,因此采用新的算法,就是我们常说的标记清除与标记整理

在早期IE中使用的GC算法是引用计数,这种算法的原理就是看对象是否还有其他引用指向它,如果没有指向对象的引用,则该对象会被视为垃圾回收器回收。但是当我们遇到循环引用的场景这种算法就会出现问题。因此目前的主流浏览器都不会使用这种方式来进行GC

标记清除

标记清除分为标记和清除两个阶段

在标记阶段会遍历堆中的所有对象,然后标记存活的对象,在清除阶段时会将死亡的对象进行清除。标记清除算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否可以被回收。

大概步骤:

  1. 垃圾回收器后见也给 GC roots,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,Windows全局对象可以看成一个根节点
  2. 然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
  3. 最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。

缺陷:

在经历过一次标记清除后,内存空间可能会出现不连续的状态,因为我们所清理的对象的内存地址可能不是连续的,所以就会出现内存碎片的问题,导致后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,而这次垃圾回收其实是没必要的,因为我们确实有很多空闲内存,只不过是不连续的。

大概意思就是我们你和你的女朋友去影院,但是影院的座位没有连号的所以你们不能坐在一起,所以你们决定明天趁没人的时候看(相当于GC),但是这种情况我们完全不用等明天,只需要将座位整理压缩一下你们就可以坐在一起了。所以就引出了一个概念——标记整理

标记整理

回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存,

新生代转向老生代

当一个对象再新生代时期经过了多次的复制之后依旧存活,那么它会被认为是一个生命周期较强的对象,在下一次进行垃圾回收时,该对象会被直接转移到老生代中,这种现象叫做对象晋升

对象晋升有两个条件:

  1. 对象是否经历了一次scavenge算法
  2. To空间的内存占比时候已经超过了25%

2.如何避免内存泄露

  1. 尽可能少地创建全局变量

  2. 手动清除定时器

  3. 少用闭包

  4. 清除DOM引用