内存寻梦环游记:一个变量的三重死亡

4,704 阅读13分钟

内存的世界

小 u 身高 64 位,是内存世界 number 家族里的一名浮点数变量。因为小 u 身体的二进制第一位是 0,所以按照 IEEE 754 标准,大家都把她当做女孩子来看待。她第 2 位到第 11 位的阶码并不够大,使得她看起来小巧玲珑;而她剩下的 52 个小数位十分精致,这样工作的时候和她打交道的变量舍入误差都很小,所以大家都很喜欢她。

小 u 每天的工作,是在内存世界里和其他的变量打交道,计算出有用的结果去造福人类世界。平时,在函数调用结束以后,小 u 就可以下班回到她在源代码里的家了。她的工作压力不大,不像那些身处 for 循环里名叫 i 呀 j 呀的变量那样需要不停地加班连轴转。而她的家也是自她出生以来就由人类世界里的程序员编写好的。别看那些程序员穿着邋遢,但对源代码却像对待自己的孩子一样宠爱。小 u 在源代码里的家就是用一种名叫 JavaScript 的材料建起来的,不光有五颜六色的编辑器主题来装饰,还有严谨的分号和括号来保证家里的结构的稳定和对称,让她很有安全感。

虽然有着可爱的外表、轻松的工作和舒心的家,但小 u 却还是有着自己的烦恼:她的家族出身决定了她不能有伴侣。

在 JavaScript 这种材料所在的国度里,number 家族隶属于古老的基本类型家族。除了 number 之外,那些经典的数据结构,像字符串 string 和空值 null,都属于基本类型家族。由于简单的基本类型很容易在代码里被解释器推断出来,所以他们的内存都是在一种死板的『栈』空间上预先分配好而不可变的。哪怕是和其他 number 耳鬓厮磨地加加减减,也不能真正地在一起。

而与基本类型家族相对的,则是时髦的引用类型家族。那些人类程序员青睐的所谓『面向对象编程』,说的就是这个家族。这个家族的成员复杂而多变,因此他们会被分配到广袤的『堆』空间上,相互之间经常是你中有我,我中有你的状态。比起注定孤独一生的基本类型家族,有对象的引用类型家族无疑要滋润得多。

小 u 有个不敢说出口的梦想,那就是努力成为引用类型里的一员。听说在远方的 Java 国度,有一条叫做『自动装箱』的法律能够让自己的家族看起来像引用类型家族一样,那样她也许就可以不再孤独了。

梦想归梦想,她对自己的生活其实还是挺满意的。在内存世界习惯之后,工作和生活的平衡是许多人类世界的程序员一辈子都达不到的。这样的生活一直继续着,直到有一天……

闭包的诅咒

那天像往常一样,小 u 从源代码的家里出发,通过词法分析门后,搭上了语法分析班车的轨道。班车上 JIT 的标识代表着 Just-In-Time,就好像人类世界中『JR 新干线』和『和谐号』那样,是高效、快捷的象征。

班车迅速地把小 u 载到了语法树轨道上的叶子节点站台。走下班车,站台上有一张 64 位尺寸的长椅。她坐上椅子闭上眼,等待着解释器对她的扫描和调用。

『但愿这次不要遇上粗俗的 null 值……』小 u 默念着,眼前一阵电光闪动,随着内存世界底层无数晶体管状态的改变,解释器如期读取了小 u 的值。在这条原子性的指令里,小 u 需要让解释器完全地控制自己,她从来不知道从电光闪动到再一次睁开眼睛之间,内存世界里发生了什么。

『嗯……』她如期醒来了,照理说她在醒来时还是会身处同样的站台位置,等待回程的语法树班车接她回家。

眼前还是同样的景象,不对,好像又有哪里不一样——站台的结构和布置似乎和之前别无二致,只是少了一样东西:轨道上空空荡荡,没有等待她的班车,更没有别人。难道……误点了?她打心里不相信这样低级的错误会出现频率精准的内存世界里。不过班车没来就是没来,她只好在站台上继续等待。

时间一赫兹一赫兹地经过,小 u 内心的不安和焦虑也在慢慢增加:到底发生了什么?班车是忘记我了吗?还是说提前开走了?女孩子一个人在外呆这么久是很不安全的,但是作为严谨的变量,独自行动更是内存世界里的大忌。『还是……再等等吧……』小 u 有些绝望地想。

班车还是没有到。

『不行了,我必须回源码里去啊!』等待终于让小 u 的情绪激动起来了,她开始在站台上寻找其它的出口,想要找到回家的路。轨道不能跳下去,但站台的两头有个红色的 Exit 标识,那里看起来是个可以通行的出口。不过现代编程语言国度里的变量一般从来都不这么走,因为手动的内存操作很危险。

小 u 打量四周,小心翼翼地推开了回程那头 Exit 下锈迹斑斑的门。谢天谢地,这里是有路的,并且看起来不是那么危险。她走过一段狭长的走道,走道里每隔固定的长度就会亮着一个小小的指示灯,看起来是内存地址空间的下标标识。终于,她看到了出口:一扇形状相同的 Exit 门。小 u 迫不及待地推开门,想看看自己有没有更接近家一点。

眼前的景象让她诧异:一模一样的轨道、一模一样的长椅、一模一样的站台、一模一样的 Exit,就好像自己根本没有移动过一样!

难道我走错路了吗?这不可能呀!小 u 对方向这样非 0 即 1 的状态有着绝对的自信,她知道她不会走错的。也许这段地址空间里的内容都是这样吧?没事的,再走走就不一样了吧。于是,天真的她开始了漫长的步行,然而让她一点点丧失信心的是,每一个 Exit 都通向同样的站台,毫无区别,甚至连锈迹都是一样的。『有人吗!』她开始呼救,尽管看起来有些徒劳。又这样支撑了一会,她终于感觉要放弃了,疲惫地坐在一个站台的长椅上听天由命。

……

『你迷路了吗?』

耳边一个声音响起,她骤然惊醒,蜷缩起来打量着声音的来源。这也是个 number 家族的浮点数,从第一位 1 来看是个男孩子,有着高她一个头的阶码和粗糙的小数位。

『你是谁……这又是哪里?』

『我是小 s,这里是闭包的堆空间。』

『闭包……堆?』

『是啊,我们家族的变量平时都是分配在栈上,每次调用的生命周期很快就能结束了。但是现在不知道在哪个函数里还有着对我们的引用,所以我们还没法被清除掉……』

『等等!生命周期是什么东西啊?难道我的生命还会结束吗?』

看到小 u 迷茫的样子,小 s 显得很吃惊:『难道你不知道吗?我们变量的生命一共有三重死亡呀。第一重,发生在我们离开作用域的时候,比如一个函数返回以后。这时候在上下文里就找不到我们了,我们这一重生命周期结束,但是不会被马上销毁掉。第二重,发生在内存中不再有引用我们的地方,解释器进行垃圾收集的时候。这时候我们彻底离开内存世界,回到源代码里。第三重,是人类世界里的程序员把我们的定义代码删除的时候,那时候才是最终的死亡。』

『那……难道我每次回到源码家里的时候,都……』

『是的,会发生前两重的死亡。但是只要源码没有被删除,我们就仍然存在于世界上。并且,前两重死亡发生得非常快,我们根本感觉不到。』

『可是,这样重新回到源码里的我还是我吗?』

『别问我这么深奥的问题啊……不过你要这么说的话,一个人还没有办法重复踏进两次河流呢!』

『噢……好像是这样……可是你刚才说的什么堆……』小 u 看起来还是很困惑。

『哦哦,你说这个啊!我们虽然是基本类型,但也不一定分配在栈上的。有可能引用类型会里动态地用到我们,这时候我们也有可能被分配在堆上呀。』小 s 还是在一本正经地说教。

闭包…引用类型…堆…小 u 恍然大悟,原来自己所在的空间,已经不是之前那个能够及时把她释放到回程班车上的栈空间了。由于某个函数或者引用类型此刻还有若干指向自己的地方,因此她被分配在了动态的堆空间上——这不就是她一直希望的吗!不过,由于解释器对堆空间的自动内存回收还没有运行,因此她现在只能和小 s 在这片空间里游荡,就好像被诅咒了一样。

『所以,我们能一起回去吗?』

循环的泄漏

『本来我们肯定可以一起回去的,可感觉好奇怪,照理说解释器早该自动把我们这一带的内存都回收了,怎么到现在还是什么都没发生……』小 s 虽然看起来博闻强识,不过对于眼前的情况还是有些困惑。

『会不会这一带还有别人在使用……』小 u 的判断力好像恢复了。

『如果按正常的内存分配,到现在应该早就自动回收了呀。除非内存泄漏……啊!』小 s 好像被自己吓到了。

『那又是什么啊?』

『说来话长了……这么说吧,内存世界里一些制度比较老的国家,是让人类世界的程序员手动把我们释放掉的。这个规矩经常漏掉一些变量,给我们带来了很大的痛苦。我们 JavaScript 这边倒好一点,可以让解释器帮我们自动回收内存……』

『欸?那不是很好吗?』

『哎呀,自动回收的代码也是那帮不靠谱的程序员写的,该有的问题还是会有的呀。比如那个蹩脚的 IE 浏览器,出现循环引用的时候就会出问题……啊对了!怪不得我们出不去了!估计我们是被困在 IE 里了!』

『循环…引用…?』

『这个简单说是这样的:假如我们不是浮点数,是引用类型的对象的话,那么只要 u 这个对象有个属性指向我,而我的一个属性指向 u,这个你中有我我中有你的情况就是循环引用了啊。』

小 u 的脸忽然红了。不过迟钝的小 s 还是滔滔不绝:『现代的浏览器做内存回收的算法普遍是标记清除算法,这个算法没有循环引用问题。但是早期 IE 用了一个叫引用计数的算法,这个算法在刚才那种情况的时候引用计数就不会清零,这样内存就不会被解释器收集了……』

『啊……所以我们回不去了吗?』

重生的重构

小 u 的疑问把小 s 从知识的海洋里拉了出来。现在,他们终于明白了现状:两个孤独的基本类型变量没有办法被自动回收,只要用户不停机,他们就会被永远困在这里,就像盗梦空间里那样。并且数学上已经证明,停机问题是不可解的。两人间长长的沉默降临了。

终于,小 s 打破了沉默:『其实……我想到了一个方法,可以试试。』

『嗯嗯,是什么啊?』

『我在的代码段应该还会执行,在那个时候,我想办法触发一个异常,让程序挂掉。』

『可是我们都好好地在这里了呀,已经是正确的代码怎么会报错呢?』

小 s 苦笑了一下:『看来你对 JavaScript 的奇葩一无所知啊。据说当初国父 Brendan Eich 制定基本国策的时候只用了一个周末,所以这门语言到处是暗坑,就算看起来结构工整规范的代码,那些人类程序员也经常写得乱七八糟。』

『所以,怎么……』

『比如说,虽然我是浮点数,但是其实因为我是在 if 里声明的,所以只要我愿意,我就能用一个叫做变量提升的设计缺陷,把我自己临时变成 undefined。』

『那样的话,类型就错了呀。』

小 s 又自信了起来:『对,只要我抓住那次机会,把这时候的我和其他变量做一次运算,就能把返回的类型从浮点数变成危险的 NaN 了。这样后面用到结果的地方肯定都不对,就算程序不崩溃,人类世界的用户或者程序员也能发现这个问题了。』

『他们发现了以后又能怎么样呢?』

『会重构掉我这段代码,然后你也可以回去了。』

『这样的话,一旦你的代码消失了,岂不是……』

『没事,很高兴认识你……』小 s 已经慢慢走到了站台一侧的边缘了,那里有一个左花括号挡住了他。他看准花括号前的地砖,使劲地踩了下去。一瞬间,变量提升就把他带出了作用域。没有过多少赫兹的时间,站台的地面下就开始摇晃,传来了燃烧着的报错对象从地下一层层抛出调用栈的声音。随着砰的一声巨响,报错对象撕裂了地面——这也是小 u 最后记得的场景了。

在记忆中的下一个镜头,她已经在回程的语法树班车上了。回到源代码里,然后等待着后面的调用,一切又似乎重新变得那么自然,好像什么都没有发生过。当然了,她所在的源代码模块里没有一个叫做 s 的变量,也许是在那个异常抛出之后就被人类加班加点地 hotfix 重构掉了吧。

几个版本之后,小 u 在一次代码优化中终于如愿以偿地成为了引用类型的属性。初来乍到的这个新源码家庭的时候,她看到这个 class 的属性里,来了一个熟悉的新成员。

『啊,u』

『啊,s』

异口同声地,他们说出了对方的名字。

END

后记

这是作者博客的第一篇小说,也是一篇开源的小说,欢迎在 Github 上提出意见和建议😀