程序内存图.
程序从启动到运行的过程
- 通过双击或者命令启动程序
- 将程序代码加载到内存中
- 编译器对程序代码转译成二进制代码
- 内存根据编译好的内容,给程序提供足够的内存空间运行
下图是java程序的内存示意图
浏览器
当我们打开一个浏览器的时候,系统就会为它分配一定内存空间供它使用.然后浏览器再为执行在它上面的一些特性,例如html,css,js,ajax,为这些内部程序提供相应的内存空间.
JS堆栈
js程序在运行的过程中主要涉及到两个内存块
- 堆区 heap
- 栈区 stack
栈区 stack
栈区,也可以叫做 栈内存.
- 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问
- 函数在栈区执行
- 遵循后进先出(LIFO)原则,新添加或待删除的元素都保存在栈的末尾,称作栈顶,另一端称作栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
JS变量的声明
我们可以从一个简单的例子开始
var num = 110;
当执行这段代码的时候,JS将会执行以下流程:
- 为变量num创建一个标识符
- 在内存中未改标识符分配一个地址
- 将110这个值存储到该内存中
如下图:
函数在栈内存中的执行
function fn1(){
console.log("函数1")
}
function fn2(){
console.log("函数2");
}
fn1();
fn2();
嵌套函数调用
function fn1(){
console.log("函数1")
}
function fn2(){
fn1();
console.log("函数2");
}
fn2();
栈溢出
由于浏览器分给程序的内存空间是有限.而从有限的空间里分出堆栈也是有限的.当我们无限制地往栈内存压入函数执行,则很快就会导致内存空间用完,出现栈溢出的问题,
- 当我们使用递归而不给递归设置结束条件,会导致栈溢出
function fn(){ fn(); } fn();
变量和引用类型数据
var meidi = {
brand:"美的",
price: 1600
}
以上代码在运行时经历以下步骤
- 为meidi创建一个标识符 meidi
- 在栈内存中为meidi分配一块内存空间
- 在堆内存中分配一个内存空间和地址
- 在分配的内存空间中存储分配的值 {}
引用变量赋值
var meidi = {
brand:"美的",
price: 1600
}
var meidi2 = meidi;
以上代码在运行时经历以下步骤
- 为meidi创建一个标识符 meidi
- 在栈内存中为meidi分配一块内存空间
- 在堆内存中分配一个内存空间和地址
- 在分配的内存空间中存储分配的值 {}
- 为meidi2创建一个标识符 meidi2
- 将meidi存储的内存地址赋值给meidi2
var meidi = {
brand:"美的",
price: 1600
}
function fn(obj){
var bridge = obj;
brige.brand = "奥玛";
}
fn(meidi)
以上代码在运行时经历以下步骤
- 预编译fn函数,为obj创建一个唯一标识符
- 为meidi创建一个标识符 meidi
- 在栈内存中为meidi分配一块内存空间
- 在堆内存中分配一个内存空间和地址
- 在分配的内存空间中存储分配的值 {}
- 将meidi存储的内存地址赋值给obj
- 将obj存储的内存地址赋值给bridge
- 修改bridge的内存地址指向的对象里的brand;
引用类型数据拷贝
- 深拷贝
- 浅拷贝
深拷贝
将对象里的所有属性递归赋值一份出来,存储到另外一个新的内存空间里.形成另外一个完全无关的对象.
实现深拷贝
var meidi = {
brand:"美的",
price: 1600,
colors: ["red","blue"]
}
function deepClone(obj) {
var target = {};
for(var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (typeof obj[key] === 'object') {
target[key] = deepClone(obj[key]);
} else {
target[key] = obj[key];
}
}
}
return target;
}
var newB = deepClone(meidi);
console.log(newB);
console.log(newB.colors === meidi.colors);
浅拷贝
浅拷贝是拷贝引用, 拷贝后的引用都是指向同一个存放数据位置的指针, 无论操作哪一个都是在操作指向在堆中的那一个数据, 所以双方都会有影响
内存管理
由于分配JS的内存是有限的.因此对有限的内存进行管理是非常有必要的.否则程序将难以正常执行.
垃圾回收引用计数
这是最简单的内存垃圾回收算法。当一个对象被 0 引用,会被标记为 "可回收内存垃圾"。
var o1 = {
o2: {
x: 1
}
};
// 创建两个对象。
// 'o1' 引用对象 'o2' 作为其属性。全部都是不可回收的。
// 'o3' 是第二个引用 'o1' 对象的变量
var o3 = o1;
o1 = 1; // 现在,原先在 'o1' 中的对象只有一个单一的引用,以变量 'o3' 来表示
// 引用对象的 'o2' 属性。
// 该对象有两个引用:一个是作为属性,另一个是 'o4' 变量
var o4 = o3.o2;
// 'o1' 对象现在只有 0 引用,它可以被作为内存垃圾回收。
// 然而,其 'o2' 属性仍然被变量 'o4' 所引用,所以它的内存不能够被释放。
o3 = '374';
o4 = null;
// 'o1' 中的 'o2' 属性现在只有 0 引用了。所以 'o1' 对象可以被回收。
标记-清除算法
为了判断是否需要释放对对象的引用,算法会确定该对象是否可获得。
标记-清除算法包含三个步骤:
- 根:一般来说,根指的是代码中引用的全局变量。就拿 JavaScript 来说,window 对象即是根的全局变量。Node.js 中相对应的变量为 "global"。垃圾回收器会构建出一份所有根变量的完整列表。
- 随后,算法会检测所有的根变量及他们的后代变量并标记它们为激活状态(表示它们不可回收)。任何根变量所到达不了的变量(或者对象等等)都会被标记为内存垃圾。
- 最后,垃圾回收器会释放所有非激活状态的内存片段然后返还给操作系统。