深入内存原理谈JS中变量存储在堆中还是栈中

目录
  • 一、装不进冰箱的大象
  • 二、影分身的字符串
  • 三、如朕亲临的 ‘奇球'
  • 四、扑朔迷离的数字
  • 五、小结:基本类型到底存在哪里?

JavaScript中基本类型存储在堆中还是栈中?

---- 不基本类型的基本类型

看到这个问题,相信大家都觉得这个题目实在基础的不能再基础了。随手百度一下,就能看到很多人说:基本类型存在栈中,引用类型存在堆中。

真的这么简单么?

一、装不进冰箱的大象

让我们看一下这段代码:

在这里,我们声明了一个67MiB大小的字符串,如果字符串真的存在栈中,这就不好解释了。毕竟,v8默认的栈区大小为984KiB。肯定是存不下的。

注:在不同时期,不同操作系统中V8对于字符串大小的限制并不相同。大概有个范围是256MiB ~ 1GiB

node --v8-options | grep -B0 -A1 stack-size

说到这里,各位是不是心里已经开始疑惑了呢。难道百度的答案不对,得用谷歌搜?

让我们看看这到底是怎么回事。

二、影分身的字符串

const BasicVarGen = function () {
    this.s1 = 'IAmString'
    this.s2 = 'IAmString'
}

let a = new BasicVarGen()
let b = new BasicVarGen()

在这里,我们声明了两个一样的对象,每个对象包括两个相同的字符串。

通过开发者工具,我们看到虽然我们声明了四个字符串,但是其内存指向了同一个地址。

备注:chrome无法查看实际地址,此处为抽象后的地址

这说明了啥?说明了四个字符串中存的是引用地址。

所以上文中那个无法装进冰箱的大象,也就好解释了。字符串并没有存到栈中,而是存到了一个别的地方,再把这个地方的地址存到了栈中。

那,让我们修改一下其中一个字符串的内容

const BasicVarGen = function () {
    this.s0 = 'IAmString'
    this.s1 = 'IAmString'
}

let a = new BasicVarGen()
let b = new BasicVarGen()
debugger
a.s0 = 'different string'
a.s2 = 'IAmString'

debugger之前的内存快照

debugger之后的内存快照

我们可以看到,a.s0 一开始内容为 ‘IAmString' ,在我们修改其内容后,地址发生了变化。

而我们新增的a.s2 其内容为 ‘IAmString' ,其地址与其他值为 ‘IAmString'  的变量保持一致。

当我们声明一个字符串时:

  1. v8内部有一个名为stringTablehashmap缓存了所有字符串,在V8阅读我们的代码,转换抽象语法树时,每遇到一个字符串,会根据其特征换算为一个hash值,插入到hashmap中。在之后如果遇到了hash值一致的字符串,会优先从里面取出来进行比对,一致的话就不会生成新字符串类。
  2. 缓存字符串时,根据字符串不同采取不同hash方式。

所以让我们梳理一下,在我们创建字符串的时候,V8会先从内存中(哈希表)查找是否有已经创建的完全一致的字符串,如果存在,直接复用。如果不存在,则开辟一块新的内存空间存进这个字符串,然后把地址赋到变量中。这也是为什么我们不能直接用下标的方式修改字符串: V8中的字符串都是不可变的。

拿出一个js的基本类型拷贝举例讲一下v8的实现逻辑和常规的大家理解的逻辑(雅文)

 例:

var a = "刘潇洒";   // V8读取字符串后,去stringTable查找是否存在 不存在 hashTable 插入 '刘潇洒' 并把'刘潇洒'的引用存入 a
var b = a; // 直接拷贝 '刘潇洒' 的引用
b = "谭雅文"; // 查找 无 存入stringTable

疑问点:

const BasicVarGen = function () {
    this.s0 = 'IAmString'
    this.s1 = 'IAmString'
}

let a = new BasicVarGen()
let b = new BasicVarGen()
debugger
a.s0 = 'different string'
a.s2 = 'IAmString'

a.s3 = a.s2+a.s0;   // 疑问点: 字符串拼接做了哪些操作?
a.s4 = a.s2+a.s

同时申请两个拼接的字符串,内容相同。

可以看到,虽然其内容相同。但是地址并不相同。而且,地址前方的Map描述也发生了变化。

字符串拼接时如果以传统方式(如 SeqString)存储,拼接操作的时间复杂度为 O(n) ,采用 绳索结构[Rope Structure] (也就是 ConsString 所采用的数据结构)可以减少拼接所花费的时间。

如果字符串是这样,那别的基本类型也是如此么?

三、如朕亲临的 ‘奇球'

说完字符串,让我们看看V8中另外一类典型的‘基本类型':oddBall

拓展自oddBalltype

让我们再做一个小实验:

我们可以看到 上图中列举的基本类型,地址也是相同的。在赋值时,也是就地复用。(而且这些拓展自oddBall的基本类型,其地址是固定的,也就是说,在V8跑起来的第一时间,不管我们有没有声明这些基本类型,他们都已经被创建完毕了。而我们声明对象时,赋的是他们的引用。这也可以解释为什么我们说基本类型是赋到栈中:在V8中,存放在 @73 的值,永远是空字符串,那么v8就可以等效把这些地址视为值本身。)

让我们看看源码,验证一下:

生成各种oddBall类型的方法,可以看出返回的是一个地址

undefined赋值给一个变量,其实赋的是地址

getRoot方法

偏移量定义的地方

四、扑朔迷离的数字

之所以叫扑朔迷离的数字,是因为还没有搞明白其分配与改变时内存分配的机制。(其内存是动态的)

数字在V8中分为 smiheapNumber

smi 直接存进内存范围为 :-2³¹ 到 2³¹-1(2³¹≈2*10⁹)的整数

heapNumber 类似字符串 不可变范围为 :所有非smi的数字

最低位用来表示是否为指针 最低位为1 则是一个指针

const o = {
  x: 42,  // Smi
  y: 4.2, // HeapNumber
};

o.x 中的 42 会被当成 Smi 直接存储在对象本身,而 o.y 中的 4.2 需要额外开辟一个内存实体存放,并将 o.y 的对象指针指向该内存实体。

如果是 32 位操作系统,用32位表示smi 可以理解,可是64位操作系统中,为什么 smi 范围也是 -2³¹ 到 2³¹-1(2³¹≈2*10⁹)?

ECMAScript 标准约定 number 数字需要被当成 64 位双精度浮点数处理,但事实上,一直使用 64 位去存储任何数字实际是非常低效的(空间低效,计算时间低效 smi大量使用位运算),所以 JavaScript 引擎并不总会使用 64 位去存储数字,引擎在内部可以采用其他内存表示方式(如 32 位),只要保证数字外部所有能被监测到的特性对齐 64 位的表现就行。

const cycleLimit = 50000
console.time('heapNumber')
const foo = { x: 1.1 };
for (let i = 0; i < cycleLimit; ++i) {
// 创建了多出来的heapNumber实例
  foo.x += 1;
}
console.timeEnd('heapNumber') // slow   

console.time('smi')
const bar = { x: 1.0 };
for (let i = 0; i < cycleLimit; ++i) {
  bar.x += 1;
}
console.timeEnd('smi')  // fast

疑问点:

const BasicVarGen = function () {

    this.smi1 = 1
    this.smi2 = 2
    this.heapNumber1 = 1.1
    this.heapNumber2 = 2.1
}
    let foo = new BasicVarGen()
    let bar = new BasicVarGen()

    debugger

    baz.obj1.heapNumber1 ++

在数字中,一个数字的值都没有修改,其他的数字地址也都变了。

五、小结:基本类型到底存在哪里?

字符串:   存在堆里,栈中为引用地址,如果存在相同字符串,则引用地址相同。

数字:        小整数存在栈中,其他类型存在堆中。

其他类型:引擎初始化时分配唯一地址,栈中的变量存的是唯一的引用。

这里只能算是大概讲明白了基本类型存在哪里,

到此这篇关于深入内存原理谈JS中变量存储在堆中还是栈中的文章就介绍到这了,更多相关JS中变量存储在堆中还是栈中内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • js变量、作用域及内存详解

    基本类型值有:undefined,NUll,Boolean,Number和String,这些类型分别在内存中占有固定的大小空间,他们的值保存在栈空间,我们通过按值来访问的. (1)值类型:数值.布尔值.null.undefined. (2)引用类型:对象.数组.函数. 如果赋值的是引用类型的值,则必须在堆内存中为这个值分配空间.由于这种值的大小不固定(对象有很多属性和方法),因此不能把他们保存到栈内存中.但内存地址大小是固定的,因此可以将内存地址保存在栈内存中. <script type="

  • JavaScript变量Dom对象的所有属性

    DOM对象的HTML: <button>Disable State</buttom> 1,遍历DOM对象所具有的属性(全部,不管是否在HTML tag中是否设置) <!DOCTYPE html> <html> <body> <button id="btnToggleState" onclick="toggleStateManagement()"> Disable State </butto

  • js变量以及其作用域详解

    一.变量的类型 Javascript和Java.C这些语言不同,它是一种无类型.弱检测的语言.它对变量的定义并不需要声明变量类型,我们只要通过赋值的形式,可以将各种类型的数据赋值给同一个变量.例如: 复制代码 代码如下: i=100;//Number类型 i="variable";//String类型 i={x:4};//Object类型 i=[1,2,3];//Array类型 JS的这种特性虽然让我们的编码更加灵活,但也带来了一个弊端,不利于Debug,编译器的弱检测让我们维护冗长的

  • JS变量提升及函数提升实例解析

    1在js中只有两种作用域 a:全局作用域 b:函数作用域 在ES6之前,js是没有块级作用域. 首先来解释一下什么是没有块级作用域? 所以此时 是可以打印输出变量a的值. 2:什么是变量提升? 在我们的js中,代码的执行时分两步走的,1.预解析 2.一步一步执行 在预解析阶段:首先会在全局作用域内,js解析器会找所有的 var .function .参数,并提前到当前作用域的最顶上去(变量的赋值操作不会提前,还在原来的地方),此时并没有执行代码. 然后再开始一行一行执行代码.遇到了函数调用,于是

  • javascript变量声明实例分析

    本文实例讲述了javascript变量声明的方法.分享给大家供大家参考.具体分析如下: js中使用一个变量之前应当先声明.变量使用关键字var来声明. 如果未在var声明语句中给变量指定初始值,则该变量值为undefined. 不用在声明变量时指定变量类型,js变量可以是任意数据类型. 使用var语句重复声明变量是合法且无害的.如果重复声明带有初始化器,则就和简单的赋值语句没啥区别. 如果试图读取一个没有声明的变量,则js会报错.在ECMAScript5严格模式下,给一个没有声明的变量赋值也会报

  • 详解JS变量存储深拷贝和浅拷贝

    变量类型与存储空间 栈内存和堆内存 基本数据类型 string.number.null.undefined.boolean.symbol(ES6新增) 变量值存放在栈内存中,可直接访问和修改变量的值 基本数据类型不存在拷贝,好比如说你无法修改数值1的值 引用类型 Object Function RegExp Math Date 值为对象,存放在堆内存中 在栈内存中变量保存的是一个指针,指向对应在堆内存中的地址. 当访问引用类型的时候,要先从栈中取出该对象的地址指针,然后再从堆内存中取得所需的数据

  • 简单谈谈JavaScript变量提升

    目录 前言 1. 什么变量提升? 2. 为什么会有变量提升? (1)提高性能 (2)容错性更好 3. 变量提升导致的问题 (1)变量被覆盖 (2)变量没有被销毁 4. 禁用变量提升 5. JS如何支持块级作用域 (1)创建执行上下文 (2)执行代码 6. 暂时性死区 总结 前言 在 ECMAScript6 中,新增了 let 和 const 关键字用来声明变量.在前端面试中也常被问到 let.const和 var 的区别,这就涉及到了变量提升.暂时性死区等知识点.下面就来看看什么是变量提升和暂时

  • JavaScript变量基本使用方法实例分析

    本文实例讲述了JavaScript变量基本使用方法.分享给大家供大家参考,具体如下: JavaScript 是一种弱类型语言,javascript的变量类型由它的值来决定. 定义变量需要用关键字 'var' var iNum = 123; var sTr = 'asd'; //同时定义多个变量可以用","隔开,公用一个'var'关键字 var iNum = 45,sTr='qwe',sCount='68'; 变量类型 5种基本数据类型: 1.number 数字类型 2.string 字

  • Javascript变量的作用域和作用域链详解

    工作这几年,js学的不是很好,正好周末有些闲时间,索性买本<js权威指南>,大名鼎鼎的犀牛书,好好的把js深入的看一看.买过这本书的第一印象就是贼厚,不过后面有一半部分都是参考手册. 一:作用域 说起变量第一个要说到的肯定就是作用域,正是因为不熟悉JS的作用域,往往就会把面向对象的作用域张冠李戴,毕竟有些东西总是习惯性的这样,但是并不是每次照搬都是可以的,那么下一个问题就来了,js到底是什么作用域,当然是函数作用域了,我们的浏览器就是一个被实例化的window对象,如果在window下定义一个

  • 深入内存原理谈JS中变量存储在堆中还是栈中

    目录 一.装不进冰箱的大象 二.影分身的字符串 三.如朕亲临的 '奇球' 四.扑朔迷离的数字 五.小结:基本类型到底存在哪里? JavaScript中基本类型存储在堆中还是栈中? ---- 不基本类型的基本类型 看到这个问题,相信大家都觉得这个题目实在基础的不能再基础了.随手百度一下,就能看到很多人说:基本类型存在栈中,引用类型存在堆中. 真的这么简单么? 一.装不进冰箱的大象 让我们看一下这段代码: 在这里,我们声明了一个67MiB大小的字符串,如果字符串真的存在栈中,这就不好解释了.毕竟,v

  • 浅谈java+内存分配及变量存储位置的区别

    Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识.一般Java在内存分配时会涉及到以下区域: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中(new 出来的对象) ◆堆:存放用new产生的数据 ◆静态域:存放在对象中用static定义的静态成员 ◆常量池:存放常量 ◆非RAM存储:硬盘等永久

  • 浅谈JS的基础类型与引用类型

    两种类型: ECMAScript变量包含两种不同类型的值:基本类型值.引用类型值: 基本类型值:指的是保存在栈内存中的简单数据段: 引用类型值:指的是那些保存在堆内存中的对象,意思是,变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,由该位置保存对象: 两种访问方式: 基本类型值:按值访问,操作的是他们实际保存的值: 引用类型值:按引用访问,当查询时,我们需要先从栈中读取内存地址,然后再顺藤摸瓜地找到保存在堆内存中的值: 两种类型复制 1.基本类型变量的复制:从一个变量向一个变量复

  • C++中静态存储区与栈以及堆的区别详解

    学习c++如果不了解内存分配是一件非常可悲的事情.而且,可以这样讲,一个C++程序员无法掌握内存.无法了解内存,是不能够成为一个合格的C++程序员的.一.内存基本构成可编程内存在基本上分为这样的几大部分:静态存储区.堆区和栈区.他们的功能不同,对他们使用方式也就不同.静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.它主要存放静态数据.全局数据和常量.栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算

  • 详细谈谈JS中的内存与变量存储

    目录 前言 JS神奇的Number 存储数字 二进制如何转换 why 0.1 + 0.2 !== 0.3? 总结 前言 在前端领域,因为大部分在跟UI打交道,内存管理是最容易被忽略的部分.如果不懂内存,就看不清很多问题的本质,也难以写出更合格的代码,本次带大家走进内存的世界. JS神奇的Number 案例一:金额的计算与传递 18.9 * 100 =1889.9999999999998 案例二:违背的数学定律 0.1 + 0.2 === 0.3 // false (function (a, b,

  • 浅谈js中的变量名和函数名重名

    今天骚凯问了一道变量名冲突的题目,感觉很有意思,顺便也复习一下预解析的一些知识,有不对的地方忘前辈大神指正,题目是这样的: var a=100; function a(){ console.log(a); } a(); 这个串代码执行完会报错 : a is not a function 问题来了,为什么会报这个错误呢? 这里涉及到函数和变量的预解析: 1)函数声明会置顶 2)变量声明也会置顶 3)函数声明比变量声明更置顶:(函数在变量上面) 4)变量和赋值语句一起书写,在js引擎解析时,会将其拆

  • 浅谈js中同名函数和同名变量的执行问题

    经测试未写成闭包形式的在同一个文件中或者不同的 js 文件中定义的同名函数,调用时会执行后面一个定义的函数.即使这样写也会执行后面一个即会弹出2: <script type="text/javascript"> function t(){ alert(1); } t(); function t(){ alert(2); } </script> 另外,定义的变量与css样式也是以后面的为准. 但是对于函数,经测试这样写却会执行前面的函数直接量即弹出1,暂时不知道是

  • 浅谈js基础数据类型和引用类型,深浅拷贝问题,以及内存分配问题

    js 深浅拷贝问题 浅拷贝一般指的是基本类型的复制 深拷贝一般指引用类型的拷贝,把引用类型的值也拷贝出来 举例 h5的sessionStorage只能存放字符串,所以要存储json时就要把json使用JSON.stringify()把json转换成string,然后再用JSON.parse()转换成json数据 缺点:JSON.parse和JSON.stringify只支持IE9+以上 解决这个问题可以使用深度比那里拷贝方法 js 中内存分配问题(堆和栈) js中基本类型类型一般是存储在栈中的.

  • 浅谈JS中逗号运算符的用法

    注意: 一.由于目前正在功读JavaScript技术,所以这里拿JavaScript为例.你可以自己在PHP中试试. 二.JavaScript语法比较复杂,因此拿JavaScript做举例. 最近重新阅读JavaScript权威指南这本书,应该说很认真的阅读,于是便想把所学的东西多记录下来.后 面本人将逐步写上更多关于本书的文章. 本文的理论知识来自于JavaScript权威指南,我这里做一下整理,或者说叫笔记. 如果你的基础够好的话,完全理解不成问题,但是如果读得有些郁闷的话,可以加我的QQ:

  • 浅谈js中的引用和复制(传值和传址)

    好像一般很少人讲到js中的引用和复制,不过弄清楚这个概念可以帮助理解很多东西 先讲一下很基础的东西,看看js中几种数据类型分别传的什么 引用:对象.数组.函数 复制:数字.布尔 字符串单独说明,因为它的特殊性,无法确定是传递引用还是复制数值(因为字符串的值是没法改变的,所以纠结这个问题也是没意义的)但是用于比较的时候显然是属于传值比较(稍后具体说比较的事) 下面讲一下在使用中的具体体现 最普通的使用就是赋值了 var a = 1; var b = a; //赋的是a的复制值 b ++; aler

随机推荐