Open oliver1204 opened 5 years ago
编程语言分为编译型语言和解释型语言两种
编译型语言
解释型语言
编译型语言的源代码在执行之前要进行完全编译,例如 Java ,如果要运行,就需要 Java 虚拟机( JVM )把源代码转换为具体平台上的机器指令去执行。
解释型语言,一边解释一边执行,很明显执行速度会慢于编译型语言。 鉴于 JavaScript 在前端执行,并且只是做一些简单的表单验证等工作,所以,长期以来,市面上的浏览器引擎都是将 JavaScript 作为解释型语言去运行。
然而,在过去的几年,JavaScript 受到了更加广泛的应用,引擎对 JavaScript 的处理速度也变得更加重要。谷歌的 V8 引擎就是为解决这一问题而诞生的。
V8 引擎引入了 Java 虚拟机技术,在代码执行前,将源代码编译生成机器码,并使用隐藏类、内联缓存等技术来提高性能,从而让 JavaScript 在 V8 引擎下的运行速度大幅度提高。与传统编译型语言不同的是,JavaScript 的编译工作发生在运行时,也就是代码执行前的几微妙时间内,而且代码并非一次性完全编译,而是在某些代码需要执行时,才会进行编译。
根据 V8 引擎的运行时理论,代码在编译阶段,最主要的工作就是确定标识符的位置( 作用域链 )和类型( 原型链 ),等到代码执行时,不再需要进行额外的查找,几个机器指令即可完成,节省了大量时间。但与 Java 这种静态类型语言稍有不同,JavaScript 是一种动态类型语言,即标识符的数据类型在编译阶段无法确定。大多数 JavaScript 引擎选择通过字符串匹配来查找属性值,每一次访问某个属性,都需要重新找到其在内存中的位置。如果属性值存取十分频繁,会严重影响性能。为了突破 JavaScript 这种天生的缺陷,V8 引擎采用了隐藏类和内联缓存相结合的机制。这种机制将那些具有相同属性名的对象划归为一类,并把初次查找的属性值缓存起来,当下次查找的时候,优先比较当前对象是否是之前的隐藏类,如果是,就直接使用,如果不是,就再创建一个隐藏类,并缓存起来。
运行时
作用域链
原型链
类型
字符串匹配
*以下内容,对象默认包括 Number、Boolean 和 String 这三种包装对象。
首先,V8 引擎在内存使用上,针对不同的操作系统做了相应的限制:32位操作系统约为700M,64为操作系统约为1.4G。 其次,V8 引擎在内存划分上大致可以分为栈内存和堆内存:栈的优势是,存取速度比堆要快,但缺点是,存在栈中的数据大小与生命周期必须是确定的;堆的优势是,可以动态分配内存,数据的生命周期也没有限制,但缺点是,由于要在运行时动态分配内存,存取速度较慢。堆是垃圾收集器活动的区域。
在 V8 引擎中,大部分的对象都是直接创建在堆上。但是,如果确定一个对象不会逃逸出方法外( 数据大小与生命周期确定 ),那就让这个对象在栈上分配内存,对象占用的内存也会随着方法的销毁而销毁。保存在栈上,从而减少了临时对象在堆上的分配数量。
V8 引擎将堆内存细分为几个不同功能的空间:
新生代内存区:新创建的对象被分配到这里,属于临时区域。 老生代内存区:临时区域的对象达到一定的生存率之后,就会被分配到这个区域,主要包括引用值和仍然在使用的基本值( 闭包或全局 )。 大对象内存区:当数据需要 1M 以上的空间,体积较大,就会被分配到这个区域。
如果再细分,新生代内存区又被平分为两部分,From 区和 To 区,任意时刻只有一个区域的内存被使用。下面" 内存回收 "会详细讲解。其实,无论如何细分,目的只有一个:更快的分配内存和回收内存。
栈内存不需要进行内存回收,因为存在栈中的数据会随着局部环境的销毁而自动清除,因此只有堆需要内存回收。
某个对象已经离开执行环境,并且不再被任何变量引用,就可将其占用的空间释放。
1、标记清除
标记清除是目前主流的内存回收的算法,主要分为两个阶段:标记阶段和清除阶段。这种算法的思想是标记不再使用的对象,然后在清除阶段回收其内存。
2、标记整理
标记整理首先对所有对象进行一次标记,将还在使用的对象压缩到内存的一端,之后,直接清理掉不再使用的内存。
3、复制算法
复制算法将现有的内存空间平分为两部分,每次只使用其中一部分,假设为 From 区和 To 区。创建对象时,会在 From 区分配内存,当垃圾收集器运行时,如果 From 区的对象还在使用,就会被复制到 To 区( 数据体积超过 1M 以上,会直接进入大对象内存区 ),之后, From 区的内存可以直接释放。这一系列完成后,From 区和 To 区会进行角色互换,继续下一次内存回收。
JavaScript 是具有自动垃圾收集机制的编程语言,内存分配与内存回收完全是自动化管理。自动垃圾收集机制的最大特点是按固定时间间隔,周期性的执行内存回收。但 JavaScript 的垃圾收集器也有自己的特点:首先,JavaScript 是单线程语言,因此垃圾收集器是串行内存回收,而多线程语言大多是并行内存回收;其次,垃圾收集器在运行时,会独占 CPU,主线程暂停执行。长时间暂停必然影响用户体验,因此 V8 引擎引入了" 标记增量 "的概念,即,将原本一口气完成的内存回收,分步完成,与主线程程序交替进行。
这种机制将堆内存分为新生代和老生代,并针对两种内存执行不同的算法。
新生代内存:串行 - 复制算法,并独占 CPU,垃圾收集器运行最为频繁。 老生代内存:串行 - 标记清除 - 标记整理,并独占 CPU。 大对象内存区也属于老生代,因此同样适用老生代内存回收的算法,垃圾收集器运行最为不频繁。 内存泄漏 不再使用的内存,没有得到及时释放,就叫做内存泄漏。
1、意外的全局变量
在函数内部没有使用 var 关键字声明变量或者 this 意外绑定 window ,会创建全局变量,造成内存泄漏。
例子:
function fn() { this.name = "Tom"; age = 20; } fn(); console.log(name); //输出:Tom console.log(age); //输出:20
*全局变量一直处于全局环境,内存不会释放,建议不要储存过大的数据。
2、闭包
闭包会使一些变量无法被及时销毁,造成内存泄漏。
function fn() { var n = 999; return function() { console.log(n); } } var f = fn(); f(); //输出:999
*关于闭包是否会造成内存泄漏,一直有争议。个人认为,内存泄露并非闭包的问题。我们会选择主动把一些变量封闭在闭包中,主要考虑的是在不污染全局环境的前提下,以后还会需要使用这些变量,这些变量占用的内存并非是没有用的。所以,闭包并不符合内存泄漏的定义。
3、引用计数垃圾收集机制
引用计数的主要思想是跟踪对象被引用的次数,目前主流引擎都不再使用这种机制。它最大的缺陷是容易造成循环引用,导致内存得不到回收。
*由于 IE9 之前的浏览器采用引用计数垃圾收集机制,所以非常容易出现内存泄漏的问题,但目前的浏览器早已抛弃这种过时的算法,那些早期 IE 浏览器特有的内存泄漏的例子,这里就不再叙述了。
编译原理
编程语言分为
编译型语言
和解释型语言
两种编译型语言的源代码在执行之前要进行完全编译,例如 Java ,如果要运行,就需要 Java 虚拟机( JVM )把源代码转换为具体平台上的机器指令去执行。
解释型语言,一边解释一边执行,很明显执行速度会慢于编译型语言。 鉴于 JavaScript 在前端执行,并且只是做一些简单的表单验证等工作,所以,长期以来,市面上的浏览器引擎都是将 JavaScript 作为解释型语言去运行。
然而,在过去的几年,JavaScript 受到了更加广泛的应用,引擎对 JavaScript 的处理速度也变得更加重要。谷歌的 V8 引擎就是为解决这一问题而诞生的。
V8 引擎引入了 Java 虚拟机技术,在代码执行前,将源代码编译生成机器码,并使用隐藏类、内联缓存等技术来提高性能,从而让 JavaScript 在 V8 引擎下的运行速度大幅度提高。与传统编译型语言不同的是,JavaScript 的编译工作发生在运行时,也就是代码执行前的几微妙时间内,而且代码并非一次性完全编译,而是在某些代码需要执行时,才会进行编译。
隐藏类和内联缓存
根据 V8 引擎的
运行时
理论,代码在编译阶段,最主要的工作就是确定标识符的位置(作用域链
)和类型(原型链
),等到代码执行时,不再需要进行额外的查找,几个机器指令即可完成,节省了大量时间。但与 Java 这种静态类型语言稍有不同,JavaScript 是一种动态类型语言,即标识符的数据类型
在编译阶段无法确定。大多数 JavaScript 引擎选择通过字符串匹配
来查找属性值,每一次访问某个属性,都需要重新找到其在内存中的位置。如果属性值存取十分频繁,会严重影响性能。为了突破 JavaScript 这种天生的缺陷,V8 引擎采用了隐藏类和内联缓存相结合的机制。这种机制将那些具有相同属性名的对象划归为一类,并把初次查找的属性值缓存起来,当下次查找的时候,优先比较当前对象是否是之前的隐藏类,如果是,就直接使用,如果不是,就再创建一个隐藏类,并缓存起来。内存管理
内存分配
*以下内容,对象默认包括 Number、Boolean 和 String 这三种包装对象。
首先,V8 引擎在内存使用上,针对不同的操作系统做了相应的限制:32位操作系统约为700M,64为操作系统约为1.4G。 其次,V8 引擎在内存划分上大致可以分为栈内存和堆内存:栈的优势是,存取速度比堆要快,但缺点是,存在栈中的数据大小与生命周期必须是确定的;堆的优势是,可以动态分配内存,数据的生命周期也没有限制,但缺点是,由于要在运行时动态分配内存,存取速度较慢。堆是垃圾收集器活动的区域。
栈内存
在 V8 引擎中,大部分的对象都是直接创建在堆上。但是,如果确定一个对象不会逃逸出方法外( 数据大小与生命周期确定 ),那就让这个对象在栈上分配内存,对象占用的内存也会随着方法的销毁而销毁。保存在栈上,从而减少了临时对象在堆上的分配数量。
堆内存
V8 引擎将堆内存细分为几个不同功能的空间:
新生代内存区:新创建的对象被分配到这里,属于临时区域。 老生代内存区:临时区域的对象达到一定的生存率之后,就会被分配到这个区域,主要包括引用值和仍然在使用的基本值( 闭包或全局 )。 大对象内存区:当数据需要 1M 以上的空间,体积较大,就会被分配到这个区域。
如果再细分,新生代内存区又被平分为两部分,From 区和 To 区,任意时刻只有一个区域的内存被使用。下面" 内存回收 "会详细讲解。其实,无论如何细分,目的只有一个:更快的分配内存和回收内存。
内存回收
回收范围
栈内存不需要进行内存回收,因为存在栈中的数据会随着局部环境的销毁而自动清除,因此只有堆需要内存回收。
如何判断对象不再使用
某个对象已经离开执行环境,并且不再被任何变量引用,就可将其占用的空间释放。
内存回收常用算法
1、标记清除
标记清除是目前主流的内存回收的算法,主要分为两个阶段:标记阶段和清除阶段。这种算法的思想是标记不再使用的对象,然后在清除阶段回收其内存。
2、标记整理
标记整理首先对所有对象进行一次标记,将还在使用的对象压缩到内存的一端,之后,直接清理掉不再使用的内存。
3、复制算法
复制算法将现有的内存空间平分为两部分,每次只使用其中一部分,假设为 From 区和 To 区。创建对象时,会在 From 区分配内存,当垃圾收集器运行时,如果 From 区的对象还在使用,就会被复制到 To 区( 数据体积超过 1M 以上,会直接进入大对象内存区 ),之后, From 区的内存可以直接释放。这一系列完成后,From 区和 To 区会进行角色互换,继续下一次内存回收。
垃圾收集器
JavaScript 是具有自动垃圾收集机制的编程语言,内存分配与内存回收完全是自动化管理。自动垃圾收集机制的最大特点是按固定时间间隔,周期性的执行内存回收。但 JavaScript 的垃圾收集器也有自己的特点:首先,JavaScript 是单线程语言,因此垃圾收集器是串行内存回收,而多线程语言大多是并行内存回收;其次,垃圾收集器在运行时,会独占 CPU,主线程暂停执行。长时间暂停必然影响用户体验,因此 V8 引擎引入了" 标记增量 "的概念,即,将原本一口气完成的内存回收,分步完成,与主线程程序交替进行。
V8 引擎的分代式垃圾回收机制
这种机制将堆内存分为新生代和老生代,并针对两种内存执行不同的算法。
新生代内存:串行 - 复制算法,并独占 CPU,垃圾收集器运行最为频繁。 老生代内存:串行 - 标记清除 - 标记整理,并独占 CPU。 大对象内存区也属于老生代,因此同样适用老生代内存回收的算法,垃圾收集器运行最为不频繁。 内存泄漏 不再使用的内存,没有得到及时释放,就叫做内存泄漏。
常见的内存泄漏
1、意外的全局变量
在函数内部没有使用 var 关键字声明变量或者 this 意外绑定 window ,会创建全局变量,造成内存泄漏。
例子:
*全局变量一直处于全局环境,内存不会释放,建议不要储存过大的数据。
2、闭包
闭包会使一些变量无法被及时销毁,造成内存泄漏。
例子:
*关于闭包是否会造成内存泄漏,一直有争议。个人认为,内存泄露并非闭包的问题。我们会选择主动把一些变量封闭在闭包中,主要考虑的是在不污染全局环境的前提下,以后还会需要使用这些变量,这些变量占用的内存并非是没有用的。所以,闭包并不符合内存泄漏的定义。
3、引用计数垃圾收集机制
引用计数的主要思想是跟踪对象被引用的次数,目前主流引擎都不再使用这种机制。它最大的缺陷是容易造成循环引用,导致内存得不到回收。
*由于 IE9 之前的浏览器采用引用计数垃圾收集机制,所以非常容易出现内存泄漏的问题,但目前的浏览器早已抛弃这种过时的算法,那些早期 IE 浏览器特有的内存泄漏的例子,这里就不再叙述了。