>  기사  >  웹 프론트엔드  >  자바스크립트 작동 방식

자바스크립트 작동 방식

小云云
小云云원래의
2017-12-05 10:30:421925검색

JavaScript 작동 방식과 관련하여 이 문서에서는 엔진, 런타임 및 호출 스택에 중점을 둘 것입니다. 두 번째 기사에서는 Google V8 JavaScript 엔진의 내부를 공개하고 더 나은 JavaScript 코드를 작성하는 방법에 대한 몇 가지 조언을 제공합니다. 또한 JavaScript 메모리 누수를 처리하는 방법에 대한 몇 가지 팁도 제공합니다. SessionStack에서는 통합 웹 애플리케이션의 메모리 누수를 유발하거나 메모리 소비를 늘리지 않도록 해야 합니다.

개요

C와 같은 일부 언어에는 malloc() 및 free() 와 같은 저수준 기본 메모리 관리 기본 요소가 있습니다. 개발자는 이러한 기본 요소를 사용하여 운영 체제 메모리를 명시적으로 할당하고 해제합니다.

반면 JavaScript는 변수(객체, 문자열)가 생성될 때 자동으로 메모리를 할당하고 이러한 변수가 사용되지 않을 때 자동으로 메모리를 해제합니다. 이 프로세스를 가비지 수집이라고 합니다. 리소스를 "자동으로" 해제하는 이 기능은 많은 혼란을 야기하여 JavaScript(및 기타 고급 언어) 개발자가 메모리 관리에 신경 쓸 수 없다고 잘못 믿게 만들었습니다. 이것은 큰 실수입니다

고급 언어를 사용하더라도 개발자는 메모리 관리에 대해 어느 정도(적어도 기본적인 이해) 이해하고 있어야 합니다. 때로는 자동 메모리 관리에 몇 가지 문제가 있을 수 있으며(예: 가비지 수집 구현에 결함이 있거나 불충분할 수 있음) 개발자는 적절한 솔루션을 찾기 위해 이러한 문제를 이해해야 합니다.

메모리 수명 주기

어떤 프로그래밍 언어를 사용하든 메모리 수명 주기는 거의 항상 동일합니다.

자바스크립트 작동 방식

다음은 주기의 각 단계에서 발생하는 일에 대한 개요입니다.

여기에 개요가 있습니다. of life 주기의 각 단계에 대한 대략적인 설명:

  • 메모리 할당 — 메모리는 운영 체제에 의해 할당되며, 이를 통해 프로그램이 메모리를 사용할 수 있습니다. C와 같은 저수준 언어에서 이는 개발자가 처리해야 하는 명시적인 작업입니다. 그러나 고급 언어에서는 이러한 작업이 개발자를 대신하여 처리됩니다.

  • 메모리를 사용하세요. 기 할당된 메모리의 실제 사용은 코드 내 변수를 조작하여 내부적으로 읽고 쓰는 방식으로 이루어집니다.

  • 메모리 해제. 사용하지 않을 때 메모리는 재할당을 위해 해제될 수 있습니다. 메모리 작업 할당과 마찬가지로 메모리 해제에도 저수준 언어의 명시적인 작업이 필요합니다.

스택과 메모리의 개념을 빠르게 이해하고 싶다면 이 시리즈의 첫 번째 기사를 읽어보세요.

메모리란 무엇인가

Javascript에서 메모리를 직접 논하기 전에, 메모리가 무엇인지, 메모리가 어떻게 작동하는지 간략하게 살펴보겠습니다.

하드웨어에서 컴퓨터 메모리에는 많은 수의 트리거 회로가 포함되어 있습니다. 각 트리거 회로에는 1비트의 데이터를 저장할 수 있는 일부 트랜지스터가 포함되어 있습니다. 트리거는 고유 식별자로 처리되므로 읽고 덮어쓸 수 있습니다. 따라서 개념적으로 컴퓨터 메모리는 거대한 읽기-쓰기 배열로 생각할 수 있습니다.

인간은 비트 연산을 사용하여 모든 생각과 연산을 표현하는 데 능숙하지 않습니다. 우리는 이러한 작은 것들을 큰 것으로 구성하고 이러한 큰 것들은 숫자를 나타내는 데 사용될 수 있습니다: 8비트는 1바이트입니다. 위 바이트는 워드(16비트, 32비트)입니다.

많은 것들이 메모리에 저장됩니다:

  1. 프로그램에 사용되는 모든 변수 및 데이터

  2. 운영 체제 코드를 포함한 프로그램 코드.

컴파일러와 운영 체제는 함께 작동하여 개발자가 대부분의 메모리 관리를 수행하도록 돕지만 내부적으로 무슨 일이 일어나고 있는지 이해하는 것이 좋습니다.

코드를 컴파일할 때 컴파일러는 원본 데이터 유형을 구문 분석하고 필요한 메모리 공간을 미리 계산합니다. 그런 다음 스택 공간에 필요한 양을 할당합니다. 함수가 호출되면 해당 메모리가 기존 메모리에 추가되기 때문에 스택 공간이라고 합니다. 즉, 함수의 내부 변수가 저장되는 공간을 가리키는 스택 프레임이 스택 상단에 추가됩니다. 종료 시 이러한 호출은 LIFO(후입선출) 순서로 제거됩니다. 예:

int n; // 4字节
int x[4]; // 4个元素的数组,每个元素4字节
double m; // 8字节

컴파일러는 메모리

4 + 4 × 4 + 8 = 28바이트가 필요하다는 것을 즉시 인식합니다.

현재 정수와 더블의 크기입니다. 약 20년 전에는 정수에는 일반적으로 2바이트만 필요했고, 복식에는 4바이트가 필요했으며, 코드는 기본 데이터 유형의 크기에 의해 제한되지 않았습니다.

컴파일러는 운영 체제와 상호 작용하여 변수를 저장하기 위해 스택에 필요한 바이트 크기를 요청하는 코드를 삽입합니다.

위의 예에서 편집자는 각 변수의 정확한 주소를 알고 있습니다. 실제로 변수 n에 쓸 때마다 내부적으로 "메모리 주소 4127963"과 같은 것으로 변환됩니다.

注意,如果我们尝试访问 x[4] 的内存(开始声明的x[4]是长度为4的数组, x[4] 表示第五个元素),我们会访问m的数据。那是因为我们正在访问一个数组里不存在的元素,m比数组中实际分配内存的最后一个元素 x[3] 要远4个字节,可能最后的结果是读取(或者覆盖)了 m 的一些位。这肯定会对其他程序产生不希望产生的结果。

자바스크립트 작동 방식

当函数调用其他函数的时候,每一个函数被调用的时候都会获得自己的栈块。在自己的栈块里会保存函数内所有的变量,还有一个程序计数器会记录变量执行时所在的位置。当函数执行完之后,会释放它的内存以作他用。

动态分配

不幸的是,事情并不是那么简单,因为在编译的时候我们并不知道一个变量将会需要多少内存。假设我们做了下面这样的事:

int n = readInput(); //读取用户的输入

...

//创建一个有n个元素的数组

 

编译器不知道这个数组需要多少内存,因为数组大小取决于用户提供的值。

因此,此时不能在栈上分配空间。程序必须在运行时向操作系统请求够用的空间。此时内存从 堆空间 中被分配。静态与动态分配内存之间的不同在下面的表格中被总结出来:

자바스크립트 작동 방식

静态分配内存与动态分配内存的区别。

为了完全理解动态内存是如何分配的,我们需要花更多的时间在 指针 上,这个可能很大程度上偏离了这篇文章的主题。如果你有兴趣学习更多的知识,那就在评论中让我知道,我就可以在之后的文章中写更多关于指针的细节。

JavaScript中的内存分配

现在我们来解释JavaScript中的第一步( 分配内存 )是如何工作的。

JavaScript在开发者声明值的时候自动分配内存。

var n = 374; // 为数值分配内存
var s = 'sessionstack'; //为字符串分配内存

var o = {
  a: 1,
  b: null
};  //为对象和它包含的值分配内存

var a = [1, null, 'str']; //为数组和它包含的值分配内存

function f(a) {
  return a + 3;
} //为函数(可调用的对象)分配内存

//函数表达式也会分配一个对象
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

 //一些函数调用也会导致对象分配
`var d = new Date(); // allocates a Date object`   //分配一个Date对象的内存

`var e = document.createElement('p');  //分配一个DOM元素的内存

//方法可以分配新的值或者对象

var s1 = 'sessionstack';
var s2 = s1.substr(0, 3);  //s2是一个新的字符串
// 因为字符串是不可变的
// JavaScript可能决定不分配内存
// 而仅仅存储 0-3的范围

var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2); 
//新的数组有4个元素是a1和a2连接起来的。

 

在JavaScript中使用内存

在JavaScript中使用被分配的内存,本质上就是对内在的读和写。

比如,读、写变量的值或者对象的属性,抑或向一个函数传递参数。

内存不在被需要时释放内存

大部分的内存管理问题都在这个阶段出现。

这里最难的任务是找出这些被分配的内存什么时候不再被需要。这常常要求开发者去决定程序中的一段内存不在被需要而且释放它。

高级语言嵌入了一个叫 垃圾回收 的软件,它的工作是跟踪内存的分配和使用,以便于发现一些内存在一些情况下不再被需要,它将会自动地释放这些内存。

不幸的是,这个过程是一个近似的过程,因为一般关于知道内存是否是被需要的问题是不可判断的(不能用一个算法解决)。

大部分的垃圾回收器会收集不再被访问的内存,例如指向它的所有变量都在作用域之外。然而,这是一组可以收集的内存空间的近似值。因为在任何时候,一个内存地址可能还有一个在作用域里的变量指向它,但是它将不会被再次访问。

垃圾收集

由于找到一些内存是否是“不再被需要的”这个事实是不可判定的,垃圾回收的实现存在局限性。本节解释必要的概念去理解主要的垃圾回收算法和它们的局限性。

内存引用

垃圾回收算法依赖的主要概念是 引用。

在内存管理的语境下,一个对象只要显式或隐式访问另一个对象,就可以说它引用了另一个对象。例如,JavaScript对象引用其Prototype( 隐式引用 ),或者引用prototype对象的属性值( 显式引用 )。

在这种情况下,“对象”的概念扩展到比普通JavaScript对象更广的范围,并且还包含函数作用域。(或者global 词法作用域

词法作用域定义变量的名字在嵌套的函数中如何被解析:内部的函数包含了父级函数的作用域,即使父级函数已经返回。

引用计数垃圾回收

这是最简单的垃圾回收算法。 一个对象在没有其他的引用指向它的时候就被认为“可被回收的”。

看一下下面的代码:

var o1 = {
  o2: {
    x: 1
  }
};

//2个对象被创建
/'o2'被'o1'作为属性引用
//谁也不能被回收

var o3 = o1; //'o3'是第二个引用'o1'指向对象的变量

o1 = 1;      //现在,'o1'只有一个引用了,就是'o3'
var o4 = o3.o2; // 引用'o3'对象的'o2'属性
                //'o2'对象这时有2个引用: 一个是作为对象的属性
                //另一个是'o4'

o3 = '374'; //'o1'原来的对象现在有0个对它的引用
             //'o1'可以被垃圾回收了。
            //然而它的'o2'属性依然被'o4'变量引用,所以'o2'不能被释放。

o4 = null;  //最初'o1'中的'o2'属性没有被其他的引用了
           //'o2'可以被垃圾回收了

 

循环引用创造麻烦

在涉及循环引用的时候有一个限制。在下面的例子中,两个对象被创建了,而且相互引用,这样创建了一个循环引用。它们会在函数调用后超出作用域,应该可以释放。然而引用计数算法考虑到2个对象中的每一个至少被引用了一次,因此都不可以被回收。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 引用 o2
  o2.p = o1; // o2 引用 o1\. 形成循环引用
}

f();

 

자바스크립트 작동 방식

标记清除算法

为了决定一个对象是否被需要,这个算法用于确定是否可以找到某个对象。

这个算法包含以下步骤。

  1. 垃圾回收器生成一个根列表。根通常是将引用保存在代码中的全局变量。在JavaScript中,window对象是一个可以作为根的全局变量。

  2. 所有的根都被检查和标记成活跃的(不是垃圾),所有的子变量也被递归检查。所有可能从根元素到达的都不被认为是垃圾。

  3. 所有没有被标记成活跃的内存都被认为是垃圾。垃圾回收器就可以释放内存并且把内存还给操作系统。

자바스크립트 작동 방식

上图就是标记清除示意。

这个算法就比之前的(引用计算)要好些,因为“一个对象没有被引用”导致这个对象不能被访问。相反,正如我们在循环引用的示例中看到的,对象不能被访问到,不一定不存在引用。

2012年起,所有浏览器都内置了标记清除垃圾回收器。在过去几年中,JavaScript垃圾回收领域中的所有改进(代/增量/并行/并行垃圾收集)都是由这个算法(标记清除法)改进实现的,但并不是对垃圾收集算法本身的改进,也没有改变它确定对象是否可达这个目标。

推荐 一篇文章 ,其中有关于跟踪垃圾回收的细节,包括了标记清除法和它的优化算法。

循环引用不再是问题

在上面的例子中(循环引用的那个),在函数执行完之后,这个2个对象没有被任何可以到达的全局对象所引用。因此,他们将会被垃圾回收器发现为不可到达的。

자바스크립트 작동 방식

尽管在这两个对象之间有相互引用,但是他们不能从全局对象上到达。

垃圾回收器的反常行为

尽管垃圾回收器很方便,但是他们有一套自己的方案。其中之一就是不确定性。换句话说,GC是不可预测的。你不可能知道一个回收器什么时候会被执行。这意味着程序在某些情况下会使用比实际需求还要多的内存。在其他情况下,在特别敏感的应用程序中,可能会出现短停顿。尽管不确定意味着不能确定回收工作何时执行,但大多数GC实现都会在分配内存的期间启动收集例程。如果没有内存分配,大部分垃圾回收就保持空闲。参考下面的情况。

  1. 执行相当大的一组分配。

  2. 这些元素中的大部分(或者所有的)都被标记为不可到达的(假设我们清空了一个指向我们不再需要的缓存的引用。)

  3. 没有更多的分配被执行。

在这种情况下,大多数垃圾回收实现都不会做进一步的回收。换句话说,尽管这里有不可达的引用变量可供回收,回收器也不会管。严格讲,这不是泄露,但结果却会占用比通常情况下更多的内存。

什么是内存泄漏

内存泄漏基本上就是不再被应用需要的内存,由于某种原因,没有被归还给操作系统或者进入可用内存池。

자바스크립트 작동 방식

编程语言喜欢不同的管理内存方式。然而,一段确定的内存是否被使用是一个不可判断的问题。换句话说,只有开发者才能弄清楚,是否一段内存可以被还给操作系统。

某些编程语言为开发者提供了释放内存功能。另一些则期待开发者清楚的知道一段内存什么时候是没用的。Wikipedia有一篇非常好的关于内存管理的文章。

4种常见的JavaScript内存泄漏

1:全局变量

JavaScript用一个有趣的方式管理未被声明的变量:对未声明的变量的引用在全局对象里创建一个新的变量。在浏览器的情况下,这个全局对象是 window 。换句话说:

function foo(arg) {
    bar = "some text";
}

 

等同于

function foo(arg) {
    window.bar = "some text";
}

 

如果 bar 被假定只在 foo 函数的作用域里引用变量,但是你忘记了使用 var 去声明它,一个意外的全局变量就被声明了。

在这个例子里,泄漏一个简单的字符串不会造成很大的伤害,但是它确实有可能变得更糟。

另外一个意外创建全局变量的方法是通过 this :

function foo() {
    this.var1 = "potential accidental global";
}

// Foo作为函数调用,this指向全局变量(window)
// 而不是undefined
foo();

 

为了防止这些问题发生,可以在你的JaveScript文件开头使用 'use strict'; 。这个可以使用一种严格的模式解析JavaScript来阻止意外的全局变量。

除了意外创建的全局变量,明确创建的全局变量同样也很多。这些当然属于不能被回收的(除非被指定为null或者重新分配)。特别那些用于暂时存储数据的全局变量,是非常重要的。如果你必须要使用全局变量来存储大量数据,确保在是使用完成之后为其赋值 null或者重新赋其他值。

2: 被遗忘的定时器或者回调

在JavaScript中使用 setInterval 是十分常见的。

大多数库,特别是提供观察器或其他接收回调的实用函数的,都会在自己的实例无法访问前把这些回调也设置为无法访问。但涉及 setInterval 时,下面这样的代码十分常见:

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每5秒执行一次

 

定时器可能会导致对不需要的节点或者数据的引用。

renderer 对象在将来有可能被移除,让interval处理器内部的整个块都变得没有用。但由于interval仍然起作用,处理程序并不能被回收(除非interval停止)。如果interval不能被回收,它的依赖也不可能被回收。这就意味着 serverData ,大概保存了大量的数据,也不可能被回收。

在观察者的情况下,在他们不再被需要(或相关对象需要设置成不能到达)的时候明确的调用移除是非常重要的。

在过去,这一点尤其重要,因为某些浏览器(旧的IE6)不能很好的管理循环引用(更多信息见下文)。如今,大部分的浏览器都能而且会在对象变得不可到达的时候回收观察处理器,即使监听器没有被明确的移除掉。然而,在对象被处理之前,要显式地删除这些观察者仍然是值得提倡的做法。例如:

var element = document.getElementById('launch-button');
var counter = 0;

function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}

element.addEventListener('click', onClick);

// 做点事

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

// 当元素被销毁
//元素和事件都会即使在老的浏览器里也会被回收

 

如今的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,先调用 removeEventListener 再删节点并非严格必要。

jQuery等框架和插件会在丢弃节点前删除监听器。这都是它们内部处理,以保证不会产生内存泄漏,甚至是在有问题的浏览器(没错,IE6)上也不会。

3: 闭包

闭包是JavaScript开发的一个关键方面:一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行时实现的不同,它可能以下面的方式造成内存泄漏:

var theThing = null;

var replaceThing = function () {

  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 引用'originalThing'
      console.log("hi");
  };

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};

setInterval(replaceThing, 1000);

 

这段代码做了一件事:每次 ReplaceThing 被调用, theThing 获得一个包含大数组和新的闭包( someMethod )的对象。同时,变量 unused 保持了一个引用 originalThing ( theThing 是上次调用 replaceThing 生成的值)的闭包。已经有点困惑了吧?最重要的事情是 一旦为同一父域中的作用域产生闭包,则该作用域是共享的。

这里,作用域产生了闭包, someMethod 和 unused 共享这个闭包中的内存。 unused 引用了 originalThing 。尽管 unused 不会被使用, someMethod 可以通过 theThing 来使用 replaceThing 作用域外的变量(例如某些全局的)。而且 someMethod 和 unused 有共同的闭包作用域, unused 对 originalThing 的引用强制 oriiginalThing 保持激活状态(两个闭包共享整个作用域)。这阻止了它的回收。

当这段代码重复执行,可以观察到被使用的内存在持续增加。垃圾回收运行的时候也不会变小。从本质上来说,闭包的连接列表已经创建了(以 theThing 变量为根),这些闭包每个作用域都间接引用了大数组,导致大量的内存泄漏。

这个问题被Meteor团队发现,他们有描述了闭包大量的细节。

4: DOM外引用

有的时候在数据结构里存储DOM节点是非常有用的,比如你想要快速更新一个表格几行的内容。此时存储每一行的DOM节点的引用在一个字典或者数组里是有意义的。此时一个DOM节点有两个引用:一个在dom树中,另外一个在字典中。如果在未来的某个时候你想要去移除这些排,你需要确保两个引用都不可到达。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};

function doStuff() {
    image.src = 'http://example.com/image_name.png';
}

function removeImage() {
    //image是body元素的子节点
    document.body.removeChild(document.getElementById('image'));

    //这个时候我们在全局的elements对象里仍然有一个对#button的引用。
    //换句话说,buttom元素仍然在内存中而且不能被回收。
}

 

当涉及DOM树内部或子节点时,需要考虑额外的考虑因素。例如,你在JavaScript中保持对某个表的特定单元格的引用。有一天你决定从DOM中移除表格但是保留了对单元格的引用。人们也许会认为除了单元格其他的都会被回收。实际并不是这样的:单元格是表格的一个子节点,子节点保持了对父节点的引用。确切的说,JS代码中对单元格的引用造成了 整个表格被留在内存中了 ,所以在移除有被引用的节点时候要当心。

我们在SessionStack努力遵循这些最佳实践,因为:

sessionStack을 프로덕션 애플리케이션에 통합하면 DOM 변경, 사용자 상호 작용, JS 예외, 스택 추적, 실패한 네트워크 요청, 디버깅 정보 등 모든 내용이 기록되기 시작합니다.

SessionStack을 사용하면 애플리케이션의 문제를 재생하고 문제가 사용자에게 미치는 영향을 확인할 수 있습니다. 이 모든 것은 애플리케이션 성능에 영향을 미치지 않습니다. 사용자는 애플리케이션 내에서 페이지를 다시 로드하거나 이동할 수 있으므로 모든 관찰자, 인터셉터 및 변수 할당을 적절하게 처리해야 합니다. 이렇게 하면 메모리 누수를 방지하고 전체 애플리케이션의 메모리 사용량을 늘릴 수 있습니다.

지금 사용해 볼 수 있는 무료 플랜입니다.

자바스크립트 작동 방식


위 내용은 JavaScript가 어떻게 작동하는지 공유하는 내용입니다. 모두에게 도움이 되기를 바랍니다.

관련 추천:

JavaScript 비동기란 무엇입니까

JavaScript의 다양한 순회 방법에 대한 자세한 설명

JavaScript에서 이에 대한 종합 분석

위 내용은 자바스크립트 작동 방식의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.