JS 从内存空间谈到垃圾回收机制
壹 ❀ 引
从事计算机相关技术工作的同学,对于内存空间相关概念多少有所耳闻,毕竟像我这种非计算机科班出身的人,对于栈堆,垃圾回收都能简单说道几句;当我明白JS 基本类型与引用类型数据存储方式不同,才对于为何要使用深拷贝恍然大悟。只是知道和深入了解是两码事,那么这篇文章从内存空间说起。
贰 ❀ 栈、堆与队列
与c语言这种底层语言不同,JavaScript并没有提供内存管理的接口,而是在创建变量时自动分配内存,当变量不再需要使用时自动释放,也就是我们所常说的垃圾回收机制。
但不管是什么程序语言,内存的声明周期都满足以下三个阶段:
a.分配你需要的内存空间
b.使用分配到的内存(读、写)
c.不需要时将其释放或归还
大部分语言对于第二步是明确的,但对于JavaScript而言三步都是隐含的,也正是因如此才让JavaScript开发者产生了不用关心内存管理的错觉。
JavaScript内存空间分为栈,堆,池,队列。其中栈存放变量,基本类型数据与指向复杂类型数据的引用指针;堆存放复杂类型数据;池又称为常量池,用于存放常量;而队列在任务队列也会使用。我们一一细说。
1.栈数据结构
栈数据结构具备FILO(first in last out)先进后出的特性,较为经典的就是乒乓球盒结构,先放进去的乒乓球只能最后取出来。我在 一篇文章看懂JS执行上下文 这篇文章中有提到执行上下文栈,它用于存放js代码在执行过程中创建的所有上下文,同样也具备FILO的特性。
在js中数据类型一般分类基本数据类型(Number Boolean Null Undefined String Symbol)与引用数据类型(Object Array Function ...),其中栈一般用于存放基本类型数据,例如以下代码在栈内存中分布:
- var a = 1;
- var b = a;
- a = 2;
可以看到基本类型数据的变量名与值都存放在栈内存中,当我们将变量a复制给b时,栈会新开内存用于存放变量b,且当我们修改变量a时对变量b不会造成任何影响,因为a与b是互不相关的两份数据。
2.堆数据结构
堆数据结构是一种无序的树状结构,同时它还满足key-value键值对的存储方式;我们只用知道key名,就能通过key查找到对应的value。比较经典的就是书架存书的例子,我们知道书名,就可以找到对应的书籍。
在js中堆内存一般用于存储引用类型的数据,需要注意的是由于引用类型的数据一般可以拓展,数据大小可变,所以存放在堆内存中;但对引用类型数据的引用地址是固定的,所以地址指向还是会存放在栈内存中。
我们通过内存图来模拟以下代码:
- var a = [1,2,3];
- var b = a;
- a.push(4);
当我们创建数组a时,栈内存中只保存了变量a与指向堆内存中数组的地址指针,而当我们将a复制给变量b时,其实只是复制了一份地址指针,两者还是指向同一数组,无论谁修改,都会影响彼此。
这便是我们熟知的浅拷贝,若想对浅拷贝与深拷贝有更深了解,欢迎阅读博主 深拷贝与浅拷贝的区别,实现深拷贝的几种方法这篇文章。
3.队列
队列具有FIFO(First In First Out)先进先出的特性,与栈内存不同的是,栈内存只存在一个出口用于数据进栈出栈;而队列有一个入口与一个出口,理解队列一个较为实际的例子就像我们排队取餐,先排队的永远能先取到餐。
在js中使用队列较为突出的就是js执行机制中的event loop事件循环,如果大家对于js事件执行机制有兴趣,可以阅读博主 JS执行机制详解,定时器时间间隔的真正含义 这篇文章,一定会让你有所收获。
叁 ❀ 垃圾回收机制
我们在前面已经说到JS内存分配回收由计算机自动完成,同时也提到了垃圾回收机制这个概念,这里来细说。
1.js中的内存回收
在js中,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。
以全局变量和局部变量来说,函数中的局部变量在函数执行结束后这些变量已经不再被需要,所以垃圾回收器会识别并释放它们。而对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以尽量少使用全局变量。
2.垃圾回收的两种模式
那么垃圾回收器是如何检测变量是否需要的呢,大体上分为两种检测手段,引用计数与标记清除。
引用计数
引用计数的判断原理很简单,就是看一份数据是否还有指向它的引用,如果没有任何对象再指向它,那么垃圾回收器就会回收,举个例子:
- // 创建一个对象,由变量o指向这个对象的两个属性
- var o = {
- name: '听风是风',
- handsome: true
- };
- // name虽然设置为了null,但o依旧有name属性的引用
- o.name = null;
- var s = o;
- // 我们修改并释放了o对于对象的引用,但变量s依旧存在引用
- o = null;
- // 变量s也不再引用,对象很快会被垃圾回收器释放
- s = null;
引用计数存在一个很大的问题,就是对象间的循环引用,比如如下代码中,对象o1与o2相互引用,即便函数执行完毕,垃圾回收器通过引用计数也无法释放它们。
- function f() {
- var o1 = {};
- var o2 = {};
- o1.a = o2; // o1 引用 o2
- o2.a = o1; // o2 引用 o1
- return;
- };
- f();
标记清除
标记清除的概念也好理解,从根部出发看是否能达到某个对象,如果能达到则认定这个对象还被需要,如果无法达到,则释放它,这个过程大致分为三步:
a.垃圾回收器创建roots列表,roots通常是代码中保留引用的全局变量,在js中,我们一般认定全局对象window作为root,也就是所谓的根部。
b.从根部出发检查所有 的roots,所有的children也会被递归检查,能从root到达的都会被标记为active。
c.未被标记为active的数据被认定为不再需要,垃圾回收器开始释放它们。
当一个对象零引用时,我们从根部一定无法到达;但反过来,从根部无法到达的不一定是严格意义上的零引用,比如循环引用,所以标记清除要更优于引用计数。
从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法,但老版本的IE6除外。
肆 ❀ 如何避免内存泄漏
我们已经知道了垃圾回收的原理,那么我们如何避免创建无法回收的对象,以至造成内存泄漏的尴尬呢?下面说说常见的四种js内存泄漏。
1.全局变量
尽可能少的去创建全局变量是js开发者的常识,但如下两种方式还是会意外的创建全局变量,第一是在函数中声明变量未使用var:
- function fn() {
- a = 1;
- };
- fn();
- window.a //
上述代码中我们在函数体内声明了一个变量a,由于未使用var声明,即便在函数体内,但它依旧是一个全局变量。我们知道全局变量等同于在window上添加属性,所以在函数执行完毕,我们依旧可以访问到它。
第二种是在函数体内通过this来创建变量:
- function fn() {
- this.a = 1;
- };
- fn();
- window.a //
我们知道,当直接调用函数fn时,等同于window.fn(),所以函数体内的this会指向window,所以本质上还是创建了一个全局变量。
当然上述问题也不是无法解决,我们可以使用严格模式来避免这个问题,试着在代码头部添加‘use strict’,你会发现a就无法访问了,因为严格模式下,全局对象指向undefined。
有时候我们无法避免使用全局变量,那么记得在使用完毕后手动释放它们,例如让变量指向null。
2.被遗忘的定时器或回调函数
- var serverData = loadData();
- setInterval(function () {
- var renderer = document.getElementById('renderer');
- if (renderer) {
- renderer.innerHTML = JSON.stringify(serverData);
- }
- }, 3000);
在上述代码中,当dom元素renderer被移除时,由于是周期定时器的缘故,定时器回调函数始终无法被回收,这也导致了定时器会一直对数据serverData保持引用,好的做法是在不需要时停止定时器。
在例如我们在使用事件监听时,如果不再需要监听记得移除监听事件。
- var element = document.getElementById('button');
- function onclick(event) {
- element.innerHTML = 'text';
- };
- element.addEventListener('click', onclick);
- // 移除监听
- element.removeEventListener('click', onclick);
3.闭包
闭包在js开发中是极其常见的,我们来看个例子:
- var theThing = null;
- var replaceThing = function () {
- var originalThing = theThing;
- var unused = function () {
- //unused未执行,但一直保持对theThing的引用
- if (originalThing)
- console.log("hi");
- };
- //创建一个新对象
- theThing = {
- longStr: new Array(1000000).join('*'),
- someMethod: function () {
- console.log("message");
- }
- };
- };
- setInterval(replaceThing, 1000);
定时器每次调用replaceThing,theThing都会获得一个包含数组longStr与闭包someMethod的新对象。
闭包unused保持着对象originalThing的引用,因为theThing赋值的缘故,也保持了对theThing的引用。虽然unused没执行,但引用关系会导致originalThing一直无法被回收,那么theThing也一样。正确做法是在replaceThing 最后添加originalThing = null;
所以我们常说,对于闭包中的变量,在不需要时一定记得手动释放。
4.DOM的引用
操作dom总是被认为是不好的,但一定得操作,我们的习惯是通过一个变量来存储它,这样就可以反复使用了,但这也会造成一个问题,dom会被引用2次。
- var elements = document.getElementById('button')
- function doStuff() {
- elements.innerHTML = '听风是风';
- };
- // 清除引用
- elements = null;
- document.body.removeChild(document.getElementById('button'));
在上述代码中,一次引用是基于dom树的引用,第二是变量elements的引用,当我们不需要这个dom时,都做两次清除操作。
伍 ❀ 参考
JavaScript 工作原理(一)——内存管理与四种常见内存泄漏的处理方法
JS 从内存空间谈到垃圾回收机制的更多相关文章
- 【Java】关于JVM运行时内存空间、JVM垃圾回收机制
参考的优秀文章 <深入理解Java虚拟机 JVM高级特性与最佳实线>(机械工业出版社) Java虚拟机的堆.栈.堆栈如何去理解? 聊聊JVM的年轻代 前言 本文是<深入理解Java虚 ...
- 浅谈 JavaScript 垃圾回收机制
github 获取更多资源 https://github.com/ChenMingK/WebKnowledges-Notes 在线阅读:https://www.kancloud.cn/chenmk/w ...
- JVM内存管理和JVM垃圾回收机制
JVM内存管理和JVM垃圾回收机制(1) 这里向大家描述一下JVM学习笔记之JVM内存管理和JVM垃圾回收的概念,JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,另外JVM分别对新生代和旧生代采 ...
- JVM的内存区域划分以及垃圾回收机制详解
在我们写Java代码时,大部分情况下是不用关心你New的对象是否被释放掉,或者什么时候被释放掉.因为JVM中有垃圾自动回收机制.在之前的博客中我们聊过Objective-C中的MRC(手动引用计数)以 ...
- JVM内存管理、JVM垃圾回收机制、新生代、老年代以及永久代
内存模型 JVM运行时数据区由程序计数器.堆.虚拟机栈.本地方法栈.方法区部分组成,结构图如下所示. JVM内存结构由程序计数器.堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)程序计数器 ...
- 浅谈python垃圾回收机制
引入 解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,那 ...
- 浅谈java垃圾回收机制
今天看thinking in java,里面很详细的谈到java垃圾回收器机制,看完后让我对这神秘的区域有一定的了解,特写一些小总结记录下来. 分两点来说. 第一点:Object.finalize() ...
- 浅谈c#垃圾回收机制(GC)
写了一个window服务,循环更新sqlite记录,内存一点点稳步增长.三天后,内存溢出.于是,我从自己的代码入手,查找到底哪儿占用内存释放不掉,最终明确是调用servicestack.ormlite ...
- java中垃圾回收机制和引用类型
在java中JDK1.2版本以后,对象的引用类型分为四种,从高到低依次为:强引用.软引用.弱引用.虚引用. ①强引用的特点:垃圾回收机制绝不会回收它,即使内存不足时,JVM宁愿抛出OutOfMemor ...
随机推荐
- MySQL数据库~~~~~索引
1. 索引 索引在MySQL中也叫"键"或者"key",是存储引擎于快速找到记录的一种数据结构. 索引的数据结构: B+树 B+树性质: 索引字段要尽量小; 索 ...
- volatile可见性案例-黑马
volatile可见性案例-黑马 package com.mozq.demo.demo; class Task implements Runnable{ //public boolean flag = ...
- JS表单内容垂直循环滚动
参考博客:https://blog.csdn.net/yubo_725/article/details/52839493 大佬是真的厉害,保存一下,以方便后续使用 效果: 源码: <!DOCT ...
- 了解angularjs中的生命周期钩子函数$onInit,$onChange,$onDestory,$postLink
壹 ❀ 引 我在前面花了三篇文章用于介绍angularjs的指令directive,组件component,并专门花了一篇文章介绍directive与component的不同,其中提到在compon ...
- vue组件化思想和模块化
组件化 注册组件的基本步骤 创建组件构造器 (调用Vue.extend()方法) 注册组件 (调用Vue.component()方法) 注册组件语法糖 省去了调用Vue.extend()的步骤,而是可 ...
- 【第二章】Zabbix3.4监控SQLServer数据库和H3C交换机思科Cisco防火墙交换机教程笔记
监控SQLServer数据库 SSMS执行相关SQL SQL模板命名规则 Zabbix客户端导入模板 添加SQLServer监控图形 SQLServer服务器关联模板 监控思科Cisco防火墙交换机 ...
- C#函数(构造函数)的重载
using System; namespace test { class Program { static void Main(string[] args) { Cat cat = new Cat() ...
- PHP中发送qq邮件
<?php namespace app\home\logic; use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Except ...
- electronr进行签名与公证
windows: 1.设置package.json的有关window打包的相关内容 "win": { "icon": "build/icons/ico ...
- Dynamics 365 Online-Delete Audit History Data
Dynamics 365 CE自带的Audit功能,虽然不会给我们的业务流程带来显著变化,但是这个功能对于我们追溯数据变化的历史,诊断定制触发的执行,以及数据还原等,都是不可或缺的关键先生.尤其是涉及 ...