字符串存储在内存栈段中吗

688 阅读6分钟

1.字符串跟数值、布尔等一样,都属于“值类型”,MDN上解释如下:

除对象类型(object)以外的其它任何类型定义的不可变的值(值本身无法被改变)。
例如(与 C 语言不同),JavaScript 中字符串是不可变的
(译注:如,JavaScript 中对字符串的操作一定返回了一个新字符串,原始字符串并没有被改变)。
我们称这些类型的值为“原始值”。

2. 简单介绍下程序中使用到的数据,可存放的内存位置。

stack栈段:程序在载入内存后,操作系统给进程分配一段连续的空间,主要用来实现函数调用,在函数执行过程中临时存储运行时需要的数据。此段空间较小,一般为几M。对于函数运行中这些临时数据的存取,有专门的寄存器sp栈顶指针寄存跟bp函数栈帧基址寄存器来进行定位,函数执行时候,将变量依次入栈,函数执行结束时候依次出栈,所以栈中的变量值在函数执行完毕,依次出栈后就不再使用。要想这些变量进行定位,我们首先要知道变量是在函数所有变量中的位置,比如是第一个声明的还是第二个声明的,这个通过我们在代码中声明的位置即可确定。其次我们要知道声明的每个变量占用多大的空间,这个我们需要通过变量的类型来确定。因此临时存储在栈中的变量有这两个特点:1是不能太大,因为空间有限。2是大小确定,因为要使用bp指针来进行定位。

heap堆(数据段):程序运行中,不能满足栈中数据的特点的那些临时数据。比如数据比较大,栈空间总共才几M,放到栈段不合适。或者数据大小不确定,比如我们通过http接口拿到的数据,谁知道多大呢,接口可能返回5条数据,可能返回10条,或者一条也没查到,这类数据必然要放到堆里,可动态扩展,不够了再多申请点。

代码段:此段除了可以放代码编译完的程序指令,还可以放全局常量。因为全局常量跟代码都有相同的特点只能读不能改。

data(数据段):可放全局变量,或者static修饰的变量。相比放在代码段的全局常量,可以进行数据修改。

下面是我在x86Mac笔记本下的c语言通过gcc编译为汇编的代码,可以参考下变量存储位置。
c源文件:

#include <stdio.h>
#include <stdlib.h>

int global_a; // 未初始化的全局变量
int global_b = 2222; // 初始化的全局变量
const char * NAME = "zhao";// 全局常量 

int main(void) {
        int a = 3333; // int变量  大小:四个字节 存储位置:栈
	char * addr = "beijing"; // 指针变量,代码段:存储字符串beijing  
                                 // 栈段: 存储字符串beijing的内存地址
        return 0;
}

编译指令:gcc -S -masm=intel test2.c -fno-asynchronous-unwind-tables
编译后的汇编文件:

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 11, 0	sdk_version 11, 3
	.intel_syntax noprefix
	.globl	_main                           ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
## %bb.0:
	push	rbp
	mov	rbp, rsp
	xor	eax, eax
	mov	dword ptr [rbp - 4], 0
	mov	dword ptr [rbp - 8], 3333  ## 这是int变量a大小四个字节,
                                           ## 栈段所处位置相比上一个数据-4
	lea	rcx, [rip + L_.str.1]   ## 这是获取L_.str.1的内存地址,
                                        ## L_.str.1在下面就是字符串beijing
	mov	qword ptr [rbp - 16], rcx ## 将获取到的L_.str.1的内存地址,
                           ## 放到栈段char * addr,大小8字节,栈段所处位置相比上一个数据-8
	pop	rbp
	ret
                                        ## -- End function
	.section	__DATA,__data
	.globl	_global_b                       ## @初始化的全局变量global_b,值是2
	.p2align	2
_global_b:
	.long	2222                            ## 0x8ae

	.section	__TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str 字符串zhao
	.asciz	"zhao"

	.section	__DATA,__data
	.globl	_NAME                           ## @ 全局常量NAME
	.p2align	3
_NAME:
	.quad	L_.str

	.section	__TEXT,__cstring,cstring_literals
L_.str.1:                               ## @.str.1 字符串beijing
	.asciz	"beijing"

	.comm	_global_a,4,2                   ## @global_a 未初始化的全局变量
.subsections_via_symbols

从编译后的汇编代码就可以很清楚的看到
a. 函数中的局部变量,都是通过bp函数栈帧基址寄存器来进行定位的。rbp-8, rbp-16 等。

     b.字符串字面量跟常量,数据都被放到section: __TEXT,后面链接时候会被映射到代码段

     c.指针类型变量,在栈中存储数据所在的内存地址,如果是64位机器,指针大小就是8个字节。

3.js中使用到的字符串。
通过文件读取或者接口返回的应该在heap里。

   对于字面量声明的比如 const str1 = 'AAAAAA'这种的,我觉得应该放在代码段。

   通过new String()生成的对象肯定是在js引擎的heap里,可是作为参数的字符串字面量呢?我觉得应该也是放在代码段。

    但是一直没找到相对权威的介绍,在chrom的devtools的文档中介绍memory面板的那部分,有一段字符串存储的介绍,地址:chrome-devtools-doc 。 截图如下。说是string可能存储在虚拟机vm heap里,或者在渲染进程的内存里,这部分数据呢,不受js虚拟机的垃圾回收器管理。这段介绍上面就是数字numbers的存储,可以看到大的数字也是存在heap里的。

4.下面是我通过chrom-devtools调试工具的memory面板,来进行vm heap采样时用到的一些过程,可简单参照下。

我的demo代码如下,比较简单,定义几个字符串,一种是普通字面量定义,一种是通过"+"连接字符串,一种是new一个String对象。

                <script>
		   function test (){
			   const str1 = 'AAAAAA';
			   const str2 = 'AAAAAA-BBBBBB';
			   const str3 = str1 + str2;
			   const str4 = new String('AAAAAA-CCCCCCC');
		   }
		   test();		</script>

5.然后打开Devtools的memory面板,在函数执行完前断点,点击“take heap snapshot”,得到内存快照如下,可以看到存放到heap内存中的各类对象,对象是以Class的constructer分组的。

5.Class很多,我们可以在Class filter那个输入框里输入 string 来过滤下,字符串相关的。经过滤后如下图,明显是有字符串的。

6.我们展开(string) 这个class下的,然后按照名称排一下序,很容易就找到了代码中声明的AAAAAA,AAAAAA-BBBBBB而且还有new String()中传的参数AAAAAA-CCCCCCC。

所以通过字面量声明字符串,即使作为函数参数的默认值,放在(string)这个类下。

7.猜也猜到了,通过“+”字符生成的连接字符串在(concatenated string)类下

8.同理通过new String()生成的String对象在String类下。

总结:

1.通过字面量声明的字符串,虽然在heap内存中,可是一直没有被gc回收,即使指向他的函数变量已经出栈了。可以通过让函数执行完毕,然后点击memory面板下的“Collect garbage” 按钮,然后再进行内存快照来验证一下。

2.js中的不同数据类型存储位置究竟是栈还是堆,最终还是要以js解释器v8引擎的实现为准。如果v8能提供一个工具,可以查看js编译为汇编代码后的样子就好了。