day038:V8 引擎如何进行垃圾内存的回收?

本文深入探讨了JS的垃圾回收机制,包括V8内存限制、新生代与老生代内存的回收策略,以及增量标记技术。揭示了JS如何在单线程环境下高效管理内存,避免内存泄漏和碎片化。

JS 语言不像 C/C++, 让程序员自己去开辟或者释放内存,而是类似Java,采用自己的一套垃圾回收算法进行自动的内存管理。作为一名资深的前端工程师,对于JS内存回收的机制是需要非常清楚, 以便于在极端的环境下能够分析出系统性能的瓶颈,另一方面,学习这其中的机制,也对我们深入理解JS的闭包特性、以及对内存的高效使用,都有很大的帮助。

V8 内存限制

在其他的后端语言中,如Java/Go, 对于内存的使用没有什么限制,但是JS不一样,V8只能使用系统的一部分内存,具体来说,在64位系统下,V8最多只能分配1.4G, 在 32 位系统中,最多只能分配0.7G。你想想在前端这样的大内存需求其实并不大,但对于后端而言,nodejs如果遇到一个2G多的文件,那么将无法全部将其读入内存进行各种操作了。

我们知道对于栈内存而言,当ESP指针下移,也就是上下文切换之后,栈顶的空间会自动被回收。但对于堆内存而言就比较复杂了,我们下面着重分析堆内存的垃圾回收。

上一篇我们提到过了,所有的对象类型的数据在JS中都是通过堆进行空间分配的。当我们构造一个对象进行赋值操作的时候,其实相应的内存已经分配到了堆上。你可以不断的这样创建对象,让 V8 为它分配空间,直到堆的大小达到上限。

那么问题来了,V8 为什么要给它设置内存上限?明明我的机器大几十G的内存,只能让我用这么一点?

究其根本,是由两个因素所共同决定的,一个是JS单线程的执行机制,另一个是JS垃圾回收机制的限制。

首先JS是单线程运行的,这意味着一旦进入到垃圾回收,那么其它的各种运行逻辑都要暂停; 另一方面垃圾回收其实是非常耗时间的操作,V8 官方是这样形容的:

以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要50ms 以上,做一次非增量式(ps:后面会解释)的垃圾回收甚至要 1s 以上。

可见其耗时之久,而且在这么长的时间内,我们的JS代码执行会一直没有响应,造成应用卡顿,导致应用性能和响应能力直线下降。因此,V8 做了一个简单粗暴的选择,那就是限制堆内存,也算是一种权衡的手段,因为大部分情况是不会遇到操作几个G内存这样的场景的。

不过,如果你想调整这个内存的限制也不是不行。配置命令如下:

// 这是调整老生代这部分的内存,单位是MB。后面会详细介绍新生代和老生代内存
node --max-old-space-size=2048 xxx.js

或者

// 这是调整新生代这部分的内存,单位是 KB。
node --max-new-space-size=2048 xxx.js

新生代内存的回收

V8 把堆内存分成了两部分进行处理——新生代内存和老生代内存。顾名思义,新生代就是临时分配的内存,存活时间短, 老生代是常驻内存,存活的时间长。V8 的堆内存,也就是两个内存之和。

根据这两种不同种类的堆内存,V8 采用了不同的回收策略,来根据不同的场景做针对性的优化。

首先是新生代的内存,刚刚已经介绍了调整新生代内存的方法,那它的内存默认限制是多少?在 64 位和 32 位系统下分别为 32MB 和 16MB。够小吧,不过也很好理解,新生代中的变量存活时间短,来了马上就走,不容易产生太大的内存负担,因此可以将它设的足够小。

那好了,新生代的垃圾回收是怎么做的呢?

首先将新生代内存空间一分为二:

其中From部分表示正在使用的内存,To 是目前闲置的内存。

当进行垃圾回收时,V8 将From部分的对象检查一遍,如果是存活对象那么复制到To内存中(在To内存中按照顺序从头放置的),如果是非存活对象直接回收即可。

当所有的From中的存活对象按照顺序进入到To内存之后,From 和 To 两者的角色对调,From现在被闲置,To为正在使用,如此循环。

那你很可能会问了,直接将非存活对象回收了不就万事大吉了嘛,为什么还要后面的一系列操作?

注意,我刚刚特别说明了,在To内存中按照顺序从头放置的,这是为了应对这样的场景:

深色的小方块代表存活对象,白色部分表示待分配的内存,由于堆内存是连续分配的,这样零零散散的空间可能会导致稍微大一点的对象没有办法进行空间分配,这种零散的空间也叫做内存碎片。刚刚介绍的新生代垃圾回收算法也叫Scavenge算法

Scavenge 算法主要就是解决内存碎片的问题,在进行一顿复制之后,To空间变成了这个样子:

是不是整齐了许多?这样就大大方便了后续连续空间的分配。

不过Scavenge 算法的劣势也非常明显,就是内存只能使用新生代内存的一半,但是它只存放生命周期短的对象,这种对象一般很少,因此时间性能非常优秀。

老生代内存的回收

刚刚介绍了新生代的回收方式,那么新生代中的变量如果经过多次回收后依然存在,那么就会被放入到老生代内存中,这种现象就叫晋升

发生晋升其实不只是这一种原因,我们来梳理一下会有那些情况触发晋升:

  • 已经经历过一次 Scavenge 回收。

  • To(闲置)空间的内存占用超过25%。

现在进入到老生代的垃圾回收机制当中,老生代中累积的变量空间一般都是很大的,当然不能用Scavenge算法啦,浪费一半空间不说,对庞大的内存空间进行复制岂不是劳民伤财?

那么对于老生代而言,究竟是采取怎样的策略进行垃圾回收的呢?

第一步,进行标记-清除。这个过程在《JavaScript高级程序设计(第三版)》中有过详细的介绍,主要分成两个阶段,即标记阶段和清除阶段。首先会遍历堆中的所有对象,对它们做上标记,然后对于代码环境中使用的变量以及被强引用的变量取消标记,剩下的就是要删除的变量了,在随后的清除阶段对其进行空间的回收。

当然这又会引发内存碎片的问题,存活对象的空间不连续对后续的空间分配造成障碍。老生代又是如何处理这个问题的呢?

第二步,整理内存碎片。V8 的解决方式非常简单粗暴,在清除阶段结束后,把存活的对象全部往一端靠拢。

由于是移动对象,它的执行速度不可能很快,事实上也是整个过程中最耗时间的部分。

增量标记

由于JS的单线程机制,V8 在进行垃圾回收的时候,不可避免地会阻塞业务逻辑的执行,倘若老生代的垃圾回收任务很重,那么耗时会非常可怕,严重影响应用的性能。那这个时候为了避免这样问题,V8 采取了增量标记的方案,即将一口气完成的标记任务分为很多小的部分完成,每做完一个小的部分就"歇"一下,就js应用逻辑执行一会儿,然后再执行下面的部分,如果循环,直到标记阶段完成才进入内存碎片的整理上面来。其实这个过程跟React Fiber的思路有点像,这里就不展开了。

经过增量标记之后,垃圾回收过程对JS应用的阻塞时间减少到原来了1 / 6, 可以看到,这是一个非常成功的改进。

JS垃圾回收的原理就介绍到这里了,其实理解起来是非常简单的,重要的是理解它为什么要这么做,而不仅仅是如何做的,希望这篇总结能够对你有所启发。

<think>嗯,用户遇到了SQL错误ORA-00932,具体是NUMBER和INTERVAL DAY TO SECOND类型不匹配的问题。我需要先理解这个错误的含义。ORA-00932通常发生在两个数据类型不一致的情况下,比如在比较或赋值操作中。用户提到的两种类型是NUMBER和INTERVAL DAY TO SECOND,这可能出现在哪里呢? 首先,检查用户可能在哪里使用了这两种类型。比如,在WHERE子句中进行比较,或者在INSERT、UPDATE语句中赋值。例如,用户可能尝试将NUMBER类型的列与INTERVAL类型的值进行比较,或者反过来。 接下来,我应该考虑如何解决这个问题。可能的解决方案包括显式转换数据类型。比如,使用TO_DSINTERVAL函数将字符串转换为INTERVAL DAY TO SECOND类型,或者在处理数值时确保两边类型一致。例如,如果用户试图将NUMBER类型的列与INTERVAL值相加,可能需要用NUMTODSINTERVAL函数将数字转换为INTERVAL。 另外,需要检查涉及到的列和表达式,确认它们的数据类型。如果用户想存储时间间隔,应确保列定义为INTERVAL DAY TO SECOND类型,而不是NUMBER。如果是计算问题,可能需要调整表达式,确保两边类型兼容。 还有,可能需要使用CAST函数显式转换类型,或者在计算时使用正确的函数。例如,当需要将数字转换为秒数的时间间隔时,可以使用NUMTODSINTERVAL(seconds, 'SECOND')。 然后,生成相关问题的时候,要考虑用户可能的后续疑问。比如如何转换其他数据类型,或者ORA-00932的其他常见原因,以及INTERVAL类型的应用场景。 需要确保回答的结构清晰,分点说明解决方案,并提供示例代码帮助用户理解。同时,引用来源要正确,虽然用户没有给出具体引用,但可能需要参考Oracle官方文档中的相关函数和错误说明。</think>### ORA-00932 数据类型不匹配解决方案 **问题原因** ORA-00932错误表示SQL语句中存在不兼容的数据类型操作,例如尝试将$NUMBER$类型与$INTERVAL\ DAY\ TO\ SECOND$类型直接比较、赋值或运算[^1]。 --- ### 解决方案分步说明 #### 1. 明确数据类型差异 - $NUMBER$:存储数值 - $INTERVAL\ DAY\ TO\ SECOND$:存储时间间隔(如`3天5小时`) #### 2. 使用显式类型转换 **场景1:将数值转换为时间间隔** 若需将数值(秒数)转换为时间间隔: ```sql SELECT NUMTODSINTERVAL(3600, 'SECOND') AS time_interval FROM dual; -- 结果:+00 01:00:00.000000 ``` **场景2:将时间间隔转换为数值** 若需提取时间间隔中的秒数: ```sql SELECT EXTRACT(SECOND FROM INTERVAL '01:30:00' HOUR TO SECOND) AS seconds FROM dual; -- 结果:5400(1.5小时=5400秒) ``` #### 3. 检查表结构定义 若字段需要存储时间间隔,应定义为`INTERVAL DAY TO SECOND`: ```sql CREATE TABLE events ( event_id NUMBER, duration INTERVAL DAY TO SECOND ); ``` #### 4. 修正表达式类型匹配 **错误示例**(混合数值与时间间隔): ```sql SELECT duration + 3600 FROM events; -- 错误 ``` **修正方法**: ```sql SELECT duration + NUMTODSINTERVAL(3600, 'SECOND') FROM events; ``` --- ### 典型错误场景与修正 | 错误场景 | 修正方案 | |---------|----------| | `WHERE number_col = INTERVAL '1' HOUR` | `WHERE number_col = EXTRACT(HOUR FROM INTERVAL '1' HOUR)` | | `SELECT number_col || INTERVAL '2' DAY` | `SELECT TO_CHAR(number_col) || INTERVAL '2' DAY` | --- 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值