首页 > 系统相关 >前端必备基础系列(五)V8引擎和内存管理

前端必备基础系列(五)V8引擎和内存管理

时间:2024-12-30 11:20:48浏览次数:1  
标签:对象 必备 JavaScript 回收 内存 V8 垃圾

浏览器的内核主要是由两部分组成的,以webkit为例:
WebCore:负责HTML解析、布局、渲染等相关工作;
JavaScriptCore:解析、执行JavaScript代码;

常见的JavaScript引擎:

  • V8 是Chrome浏览器和Node.js的JavaScript引擎
  • JavaScriptCore:是Webkit浏览器引擎的一部分,主要用于Apple的Safari浏览器,被用在IOS设备中。
  • Chakra:最初是IE9的JS引擎,后续成为Edge浏览器的引擎,直到微软转向Chromium架构并采用了V8.
  • SpiderMonkey:第一款JS引擎,由JS作者开发。

以下内容均以V8为例。

V8是⽤C ++编写的Google开源⾼性能JavaScript和WebAssembly引擎,它⽤于Chrome和Node.js等。
它实现ECMAScript和WebAssembly,并在Windows 7或更⾼版本,macOS 10.12+和使⽤x64,IA-32,ARM
或MIPS处理器的Linux系统上运⾏。
V8可以独⽴运⾏,也可以嵌⼊到任何C ++应⽤程序中。

JavaScript代码是如何被执⾏的?V8引擎如何执⾏JavaScript代码?

摘自coderwhy的课程资料

解析 (Parse):JavaScript 代码⾸先被解析器处理,转化为抽象语法树(AST)。这是代码编译的初步阶段,主要转换代码结构为内部可进⼀步处理的格式。

AST:抽象语法树(AST)是源代码的树形表示,⽤于表示程序结构。之后,AST 会被进⼀步编译成字节码。

Ignition:Ignition 是 V8 的解释器,它将 AST 转换为字节码。字节码是⼀种低级的、⽐机器码更抽象的代码,它可以快速执⾏,但⽐直接的机器码慢。

字节码(Bytecode):字节码是介于源代码和机器码之间的中间表示,它为后续的优化和执⾏提供了⼀种更标准化的形式。字节码是由 Ignition ⽣成,可被直接解释执⾏,同时也是优化编译器 TurboFan 的输⼊。

TurboFan:TurboFan 是 V8 的优化编译器,它接收从 Ignition ⽣成的字节码并进⾏进⼀步优化。⽐如如果⼀个函数被多次调⽤,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提⾼代码的执⾏性能。当然还会包括很多其他的优化⼿段,如内联替换(Inlining)、死代码消除(Dead Code Elimination)和循环展开(Loop Unrolling)等,以提⾼代码执⾏效率。

机器码:经过 TurboFan 处理后,字节码被编译成机器码,即直接运⾏在计算机硬件上的低级代码。这⼀步是将JavaScript 代码转换成 CPU 可直接执⾏的指令,⼤⼤提⾼了执⾏速度。

运⾏时优化:在代码执⾏过程中,V8 引擎会持续监控代码的执⾏情况。如果发现之前做的优化不再有效或者有更优的执⾏路径,它会触发去优化(Deoptimization)。去优化是指将已优化的代码退回到优化较少的状态,然后�新编译以适应新的运⾏情况。

V8引擎包括哪些部分,它们的作⽤是什么?
V8引擎本身的源码⾮常复杂,⼤概有超过100w⾏C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执⾏的:
Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
将代码转化成AST树是⼀个⾮常常⻅的操作,⽐如在Babel、Vue源码中都需要进⾏这样的操作;
Parse的V8官⽅⽂档:https://v8.dev/blog/scanner
Ignition是⼀个解释器,会将AST转换成ByteCode(字节码)
同时会收集TurboFan优化所需要的信息(⽐如函数参数的类型信息,有了类型才能进⾏真实的运算);
如果函数只调⽤⼀次,Ignition会解释执⾏ByteCode;
Ignition的V8官⽅⽂档:https://v8.dev/blog/ignition-interpreter
TurboFan是⼀个编译器,可以将字节码编译为CPU可以直接执⾏的机器码;
如果⼀个函数被多次调⽤,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提⾼代码的执⾏性能;
但是,机器码实际上也会被还原为ByteCode( Deoptimization 去优化),这是因为如果后续执⾏函数的过程中,类型发⽣了变化(⽐如sum函数原来执⾏的是number类型,后来执⾏变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
TurboFan的V8官⽅⽂档:https://v8.dev/blog/turbofan-jit

词法分析(英⽂lexical analysis)
将字符序列转换成token序列的过程。
token是记号化(tokenization)的缩写
词法分析器(lexical analyzer,简称lexer),也叫扫描器(scanner)
语法分析(英语:syntactic analysis,也叫 parsing)
语法分析器也可以称之为parser。

那么我们的JavaScript源码是如何被解析(Parse过程)的呢?
Blink将源码交给V8引擎,Stream获取到源码并且进⾏编码转换;
Scanner会进⾏词法分析(lexical analysis),词法分析会将代码转换成tokens;
接下来tokens会被转换成AST树,经过Parser和PreParser:
Parser就是直接将tokens转成AST树架构;
PreParser称之为预解析,为什么需要预解析呢?
预解析⼀⽅⾯的作⽤是快速检查⼀下是否有语法错误,另⼀⽅⾯也可以进⾏代码优化。
这是因为并不是所有的JavaScript代码,在⼀开始时就会被执⾏。那么对所有的JavaScript代码进⾏解析,必然会影响⽹⻚的运⾏效率;
所以V8引擎就实现了Lazy Parsing(延迟解析)的⽅案,它的作⽤是将不必要的函数进⾏预解析,
也就是只解析暂时需要的内容,⽽对函数的全�解析是在函数被调⽤时才会进⾏;
⽐如我们在⼀个函数outer内部定义了另外⼀个函数inner,那么inner函数就会进⾏预解析;
⽣成AST树后,会被Ignition转成字节码(bytecode)并且可以执⾏字节码,之后的过程就是代码
的执⾏过程(后续会详细分析)。

垃圾回收机制

因为内存的⼤⼩是有限的,所以当内存不再需要的时候,我们需要对其进⾏释放,以便腾出更多的内存空间。
在⼿动管理内存的语⾔中,我们需要通过⼀些⽅式⾃⼰来释放不再需要的内存,⽐如free函数:
但是这种管理的⽅式其实⾮常的低效,影响我们编写逻辑的代码的效率;
并且这种⽅式对开发者的要求也很⾼,并且⼀不⼩⼼就会产⽣内存泄露(memory leaks),�指针(dangling pointers);
所以⼤部分现代的编程语⾔都是有⾃⼰的垃圾回收机制:
垃圾回收的英⽂是Garbage Collection,简称GC;
对于那些不再使⽤的对象,我们都称之为是垃圾,它需要被回收,以释放更多的内存空间;
⽽我们的语⾔运⾏环境,⽐如Java的运⾏环境JVM,JavaScript的运⾏环境js引擎都会内存 垃圾回收器;
垃圾回收器我们也会简称为GC,所以在很多地⽅你看到GC其实指的是垃圾回收器;
⾃动垃圾回收提⾼了开发效率,使开发者可以更多地关注业务逻辑的实现⽽⾮内存管理的细节。
这在管理复杂数据结构和⼤�数据时⾮常�要。
但是这⾥⼜出现了另外⼀个很关键的问题:GC怎么知道哪些对象是不再使⽤的呢?
这⾥就要⽤到GC的实现以及对应的算法;

常⻅的GC算法 – 引⽤计数(Reference counting)
引⽤计数垃圾回收(Reference Counting):
每个对象都有⼀个关联的计数器,通常称为“引⽤计数”。
当⼀个对象有⼀个引⽤指向它时,那么这个对象的引⽤就+1;
如果另⼀个变�也开始引⽤该对象,引⽤计数加1;如果⼀个变�停⽌引⽤该对象,引⽤计数减1。
当⼀个对象的引⽤为0时,这个对象就可以被销毁掉;
这个算法有⼀个很⼤的弊端就是会产⽣循环引⽤,当然我们可以通过⼀些⽅案,⽐如弱引⽤来解决(WeakMap就
是弱引⽤);

常⻅的GC算法 – 标记清除(mark-Sweep)
标记清除:
标记清除的核⼼思路是可达性(Reachability)
这个算法是设置⼀个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引⽤到的对象,对于哪些没有引⽤到的对象,就认为是不可⽤的对象;
在这个阶段,垃圾回收器标记所有可达的对象,之后,垃圾回收器遍历所有的对象,收集那些在标记阶段未被标记为可达的对象。
这些对象被视为垃圾,因它们不再被程序中的其他活跃对象或根对象所引⽤。
这个算法可以很好的解决循环引⽤的问题;

其他GC算法补充
JS引擎⽐较⼴泛的采⽤的就是可达性中的标记清除算法,当然类似于V8引擎为了进⾏更好的优化,它在算法的实现
细节上也会结合⼀些其他的算法。
标记整理(Mark-Compact) 和“标记-清除”相似;
不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从⽽整合空闲空间,避免内存碎⽚化;
分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。
许多对象出现,完成它们的⼯作并很快死去,它们可以很快被清理;
那些⻓期存活的对象会变得“⽼旧”,⽽且被检查的频次也会减少;
增�收集(Incremental collection)
如果有许多对象,并且我们试图⼀次遍历并标记整个对象集,则可能需要⼀些时间,并在执⾏过程中带来明显的延迟。
所以引擎试图将垃圾收集⼯作分成⼏部分来做,然后将这⼏部分会逐⼀进⾏处理,这样会有许多微⼩的延迟⽽不是⼀个⼤的延迟;
闲时收集(Idle-time collection)
垃圾收集器只会在 CPU 空闲时尝试运⾏,以减少可能对代码执⾏的影响。

事实上,V8引擎为了提供内存的管理效率,对内存进⾏⾮常详细的划分:

新⽣代空间 (New Space / Young Generation)
作⽤:主要⽤于存放⽣命周期短的⼩对象。这部分空间较⼩,但对象的创建和销毁都⾮常频繁。
组成:新⽣代内存被分为两个半空间:From Space 和 To Space。
初始时,对象被分配到 From Space 中。
使⽤复制算法(Copying Garbage Collection)进⾏垃圾回收。
当进⾏垃圾回收时,活动的对象(即仍然被引⽤的对象)被复制到 To Space 中,⽽⾮活动的对象(不再被引⽤的对象)被丢弃。
完成复制后,From Space 和 To Space 的⻆⾊互换,新的对象将分配到新的 From Space 中,原 ToSpace 成为新的 From Space。
⽼⽣代空间 (Old Space / Old Generation)
作⽤:存放⽣命周期⻓或从新⽣代晋升过来的对象。
当对象在新⽣代中经历了⼀定数�的垃圾回收周期后(通常是⼀到两次),且仍然存活,它们被认为是⽣命周期较⻓的对象。
分为三个主要区域:
⽼指针空间 (Old Pointer Space):主要存放包含指向其他对象的指针的对象。
⽼数据空间 (Old Data Space):⽤于存放只包含原始数据(如数值、字符串)的对象,不含指向其他对象的指针。
⼤对象空间 (Large Object Space):⽤于存放⼤对象,如超过新⽣代⼤⼩限制的数组或对象。
这些对象直接在⼤对象空间中分配,避免在新⽣代和⽼⽣代之间的复制操作。
代码空间 (Code Space) :存放编译后的函数代码。
单元空间 (Cell Space):⽤于存放⼩的数据结构如闭包的变�环境。
属性单元空间 (Property Cell Space):存放对象的属性值
主要针对全局变�或者属性值,对于访问频繁的全局变�或者属性值来说,V8在这⾥存储是为了提⾼它的访问效率。
映射空间 (Map Space):存放对象的映射(即对象的类型信息,描述对象的结构)。
当你定义⼀个 Person 构造函数时,可以通过它创建出来person1和person2。
这些实例(person1 和 person2)本身存储在堆内存的相应空间中,具体是新⽣代还是⽼⽣代取决于它们的⽣命周期和⼤⼩。
每个实例都会持有⼀个指向其映射的指针,这个映射指明了如何访问 name 和 age 属性(⽬的是访问属性效果变⾼)。
堆内存 (Heap Memory) 与 栈 (Stack)
堆内存:JavaScript 对象、字符串等数据存放的区域,按照上述分类进⾏管理。
栈:⽤于存放执⾏上下⽂中的变�、函数调⽤的返回地址(继续执⾏哪⾥的代码)等,栈有助于跟踪函数调⽤的顺序和局部变�。

什么是垃圾回收机制?并且它是如何在现代编程语⾔中管理内存的?

虽然随着硬件的发展,⽬前计算机的内存已经⾜够⼤,但是随着任务的增多依然可能会⾯临内存紧缺的问题,因此管理内存依然⾮常�要。(前提)

垃圾回收(Garbage Collection, GC)是⾃动内存管理的⼀种机制,它帮助程序⾃动释放不再使⽤的内存。在不需要⼿动释放内存的现代编程语⾔中,垃圾回收机制扮演着⾮常�要的⻆⾊,通过⾃动识别和清除“垃圾”数据来防⽌内存泄漏,从⽽管理内存资源。(作⽤)

在运⾏时,垃圾回收机制主要通过追踪每个对象的⽣命周期来⼯作。(原理)

对象通常在它们不再被程序的任何部分引⽤时被视为垃圾。
⼀旦这些对象被识别,垃圾回收器将⾃动回收它们占⽤的内存空间,使这部分内存可以�新被分配和使⽤。
垃圾回收机制有⼏种不同的实现⽅法,最常⻅的包括(实现,可以先回答⼏种,表示你对GC的理解,等下再回答
V8的内容):
引⽤计数:每个对象都有⼀个与之关联的计数器,记录引⽤该对象的次数。当引⽤计数变为零时,意味着没有任何引⽤指向该对象,因此可以安全地回收其内存。
标记-清除:这种⽅法通过从根对象集合开始,标记所有可达的对象。所有未被标记的对象都被视为垃圾,并将被清除。
标记-整理:与标记-清除相似,但在清除阶段,它还会移动存活的对象,以减少内存碎⽚。

V8引擎的垃圾回收机制具体是如何⼯作的?
V8引擎使⽤了⼀种⾼度优化的垃圾回收机制来管理内存采⽤了标记-清除、标记整理,同时⼜结合了多种策略来实现⾼效的内存管理,包括结合了分代回收(Generational Collection)和增�回收(Incremental Collection)等多种策略。

分代回收:V8将对象分为“新⽣代”和“⽼⽣代”。新⽣代存放⽣命周期短的⼩对象,使⽤⾼效的复制式垃圾回收算法;⽽⽼⽣代存放⽣命周期⻓或从新⽣代晋升⽽来的对象,使⽤标记-清除或标记-整理算法。这种分代策略减少了垃圾回收的总体开销,尤其是针对短命对象的快速回收。
增量回收:为了减少垃圾回收过程中的停顿时间,V8实现了增�回收。这意味着垃圾回收过程被分解为许多⼩步骤,这些⼩步骤穿插在应⽤程序的执⾏过程中进⾏。这有助于避免⻓时间的停顿,改善了应⽤程序的响应性和性能。
延迟清理和空闲时间收集:V8还尝试在CPU空闲时进⾏垃圾回收,以进⼀步减少对程序执⾏的影响。
这些技术的结合使得V8能够在执⾏JavaScript代码时有效地管理内存,同时最⼩化垃圾回收对性能的影响。

JavaScript有哪些操作可能引起内存泄漏?如何在开发中避免?

在 JavaScript 中,内存泄漏通常是指程序中已经不再需要使⽤的内存,由于某些原因未被垃圾回收器回收,从⽽导
致可⽤内存逐渐减少。
这些内存泄漏通常是由不当的编程实践引起的,常⻅的引起内存泄漏的操作有如下情况:
全局变量滥⽤:创建的全局变�(例如,忘记使⽤ var , let , 或 const 声明变�)可能会导致这些变�不被回收。
未清理的定时器和回调函数:⽐如使⽤ setInterval 在不适⽤时没有及时清除,阻⽌它们被回收。
闭包:闭包可以维持对外部函数作⽤域的引⽤,如果这些闭包⼀直存活,它们引⽤的外部作⽤域(及其变�)也⽆法被回收。
DOM 引⽤:JavaScript 对象持有已从 DOM 中删除的元素的引⽤,这会阻⽌这些 DOM 元素的内存被释放。
监听器的回调:在使⽤完毕后没有从 DOM 元素上移除事件监听器,这可能导致内存泄漏。
开发中如何避免呢?�要的是平时在开发中就要尽�按照规范来编写代码。
使⽤局部变量:尽�使⽤局部变�,避免⽆限制地使⽤全局变�。
及时清理:使⽤ clearInterval 或 clearTimeout 取消不再需要的定时器。
优化闭包的使⽤:理解闭包和它们的⼯作⽅式。只保留必要的数据在闭包中,避免循环引⽤。
谨慎操作 DOM 引⽤:当从 DOM 中移除元素时,确保删除所有相关的 JavaScript 引⽤。包括DOM元素监听器的移除。
⼯具和检测:利⽤浏览器的开发者⼯具进⾏性能分析和内存分析。
代码审查:定期进⾏代码审查,关注那些可能导致内存泄漏的编程实践,⽐如对全局变�的使⽤、事件监听器的添加与移除等。

标签:对象,必备,JavaScript,回收,内存,V8,垃圾
From: https://www.cnblogs.com/codeyx/p/18639638

相关文章

  • 野指针、空指针、空悬指针与内存管理
    野指针、空指针、空悬指针野指针定义:指向一块未知区域(已经销毁或访问内存受限的内存区域外的已存在或不存在的内存区域),的指针,被称作野指针。野指针是危险的。危害:引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentationfault),也有可能编译运行不报错。引用......
  • 为什么说当今社会需要高级前端?高级前端需必备哪些技能?
    在当今社会,高级前端开发工程师的需求日益凸显,这主要归因于互联网行业的深入发展以及技术的不断进步。高级前端开发工程师在项目开发、用户体验优化、技术创新等方面发挥着至关重要的作用。以下是详细说明及高级前端必备的技能:一、当今社会需要高级前端的原因技术革新与行业发展......
  • Spring Boot引起的“堆外内存泄漏”排查及经验总结10
    背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于SpringBoot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M-......
  • Spring Boot引起的“堆外内存泄漏”排查及经验总结11
    背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于SpringBoot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M-......
  • Spring Boot引起的“堆外内存泄漏”排查及经验总结15
    背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于SpringBoot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M-......
  • Spring Boot引起的“堆外内存泄漏”排查及经验总结13
    背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于SpringBoot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M-......
  • Spring Boot引起的“堆外内存泄漏”排查及经验总结4
    背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于SpringBoot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M-......
  • Spring Boot引起的“堆外内存泄漏”排查及经验总结12
    背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于SpringBoot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M-......
  • Spring Boot引起的“堆外内存泄漏”排查及经验总结14
    背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于SpringBoot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M-......
  • 【Mybatis歌剧院】开发人员常用的操作数据库效率贼快的框架——Mybatis, 工作常用,面试
    本篇会加入个人的所谓鱼式疯言❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言而是理解过并总结出来通俗易懂的大白话,小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.......