관련 권장 사항: "javascript 비디오 튜토리얼"
대부분의 경우 JS 엔진이 이 문제를 처리해 주기 때문에 메모리 관리에 대한 지식을 이해하지 못한 채 개발을 진행합니다. 그러나 때때로 메모리 누수와 같은 문제가 발생하는 경우에는 메모리 할당이 어떻게 작동하는지 알아야만 이러한 문제를 해결할 수 있습니다.
이 글에서는 주로메모리 할당및가비지 수집작동 방식과 일반적인메모리 누수문제를 방지하는 방법을 소개합니다.
JS에서는 변수, 함수 또는 객체를 만들 때 JS 엔진이 메모리를 할당하고 더 이상 필요하지 않으면 해제합니다.
메모리 할당은 메모리 공간을 예약하는 프로세스인 반면,메모리 해제는 다른 목적을 위해 공간을 확보합니다.
변수를 할당하거나 함수를 생성할 때마다 해당 변수의 저장은 동일한 단계를 거칩니다.
메모리 할당
메모리 사용
Release memory
메모리 관리 맥락에서 "객체"에는 JS 객체뿐만 아니라 함수 및 함수 범위도 포함됩니다.
이제 우리는 JS에서 정의한 모든 것에 대해 엔진이 메모리를 할당하고 더 이상 필요하지 않을 때 해제한다는 것을 알았습니다.
내 마음에 떠오르는 다음 질문은 이 물건들이 어디에 저장될 것인가 하는 것입니다.
JS 엔진은메모리 힙과스택두 곳에 데이터를 저장할 수 있습니다. 힙과 스택은 엔진에서 서로 다른 목적으로 사용하는 두 가지 데이터 구조입니다.
스택은 JS에서 정적 데이터를 저장하는 데 사용하는 데이터 구조입니다. 정적 데이터는 엔진이 컴파일 타임에 크기를 알고 있는 데이터입니다. JS에서는 여기에는 객체와 함수를 가리키는 기본 값(strings
,number
,boolean
,undefined
和null
)과 참조 유형이 포함됩니다.
엔진은 크기가 변경되지 않는다는 것을 알고 있으므로 각 값에 대해 고정된 양의 메모리를 할당합니다.
실행 직전에 메모리를 할당하는 과정을정적 메모리 할당이라고 합니다. 이러한 값과 전체 스택의 제한은 브라우저에 따라 다릅니다.
힙은 JS가객체및함수를 저장하는 데이터가 저장되는 또 다른 공간입니다.
스택과 달리 JS 엔진은 이러한 객체에 고정된 양의 메모리를 할당하지 않고 필요에 따라 공간을 할당합니다. 이러한 메모리 할당 방법을동적 메모리 할당이라고도 합니다.
이 두 저장소의 특성은 아래에서 비교됩니다.
Stack | Heap |
---|---|
기본 유형 및 참조를 저장합니다. 크기는 컴파일 타임에 알려집니다. 고정된 양의 메모리를 할당합니다. |
Object 그리고 기능은 런타임에만 크기를 알 수 있습니다 제한이 없습니다 |
이미지를 향상하기 위해 몇 가지 예를 들어보겠습니다.
const person = { name: 'John', age: 24, };
JS는 힙에서 이 객체에 대한 메모리를 할당합니다. 실제 값은 여전히 원래 값이므로 스택에 저장됩니다.
const hobbies = ['hiking', 'reading'];
배열도 객체이므로 힙에 저장됩니다.
let name = 'John'; // 为字符串分配内存 const age = 24; // 为字分配内存 name = 'John Doe'; // 为新字符串分配内存 const firstName = name.slice(0,4); // 为新字符串分配内存
초기 값은 불변이므로 JS는 원래 값을 변경하지 않고 새 값을 생성합니다.
모든 변수는 먼저스택을 가리킵니다. 기본 값이 아닌 경우스택
에는힙
의 개체에 대한 참조가 포함됩니다.堆栈
包含对堆
中对象的引用。
堆的内存没有按特定的方式排序,所以我们需要在堆栈中保留对其的引用。 我们可以将引用
视为地址,并将堆中的对象视为这些地址所属的房屋。
请记住,JS 将对象和函数存储在堆中。 基本类型和引用存储在堆栈中。
这张照片中,我们可以观察到如何存储不同的值。 注意person
和newPerson
都如何指向同一对象。
const person = { name: 'John', age: 24, };
这将在堆中创建一个新对象,并在堆栈中创建对该对象的引用。
现在,我们知道 JS 如何为各种对象分配内存,但是在内存生命周期,还有最后一步:释放内存。
就像内存分配一样,JavaScript引擎也为我们处理这一步骤。 更具体地说,垃圾收集器负责此工作。
一旦 JS 引擎识别变量或函数不在被需要时,它就会释放它所占用的内存。
这样做的主要问题是,是否仍然需要一些内存是一个无法确定的问题,这意味着不可能有一种算法能够在不再需要那一刻立即收集不再需要的所有内存。
一些算法可以很好地解决这个问题。 我将在本节中讨论最常用的方法:引用计数
和标记清除
算法。
当声明了一个变量并将一个引用类型值赋值该变量时,则这个值的引用次数就是1
。如果同一个值又被赋给另外一个变量,则该值得引用次数加1
。相反,如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减1
。
当这个值的引用次数变成0
时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。
我们看下面的例子。
请注意,在最后一帧中,只有hobbies
留在堆中的,因为最后引用的是对象。
引用计数
算法的问题在于它不考虑循环引用。 当一个或多个对象互相引用但无法再通过代码访问它们时,就会发生这种情况。
let son = { name: 'John', }; let dad = { name: 'Johnson', } son.dad = dad; dad.son = son; son = null; dad = null;
由于父对象相互引用,因此该算法不会释放分配的内存,我们再也无法访问这两个对象。
它们设置为null
不会使引用计数算法识别出它们不再被使用,因为它们都有传入的引用。
标记清除算法对循环依赖性有解决方案。 它检测到是否可以从root
对象访问它们,而不是简单地计算对给定对象的引用。
浏览器的root
是window
对象,而NodeJS中的root
是global
。
该算法将无法访问的对象标记为垃圾,然后对其进行扫描(收集)。 根对象将永远不会被收集。
这样,循环依赖关系就不再是问题了。在前面的示例中,dad
对象和son
참조
를 주소로 생각하고, 힙에 있는 객체를 해당 주소가 속한 주택으로 생각할 수 있습니다.
JS는
functions
을 힙에 저장한다는 점을 기억하세요. 기본 유형과 참조는 스택에 저장됩니다.person
과
newPerson
이 모두 동일한 객체를 가리키는 방식에 주목하세요.
Example
users = getUsers();
Reference Counting
및
Mark Sweep
알고리즘에 대해 논의하겠습니다. 참조 횟수변수가 선언되고 해당 변수에 참조 유형 값이 할당되면 이 값에 대한 참조 횟수는
1
입니다. 동일한 값이 다른 변수에 할당되면 해당 값의 참조 횟수가
1
씩 증가합니다. 반대로, 이 값에 대한 참조를 포함하는 변수가 다른 값을 사용하는 경우 이 값에 대한 참조 수는
1
만큼 감소합니다. 이 값에 대한 참조 횟수가
0
이 되면 더 이상 이 값에 접근할 수 없다는 의미이므로 차지하는 메모리 공간을 회수할 수 있습니다. 이렇게 하면 다음에 가비지 수집기가 실행될 때 참조가 0인 값이 차지한 메모리를 해제하게 됩니다. 아래 예를 살펴보겠습니다.
취미
만 힙에 남아 있습니다. 주기 계산
참조 계산
알고리즘의 문제점은 순환 참조를 고려하지 않는다는 것입니다. 이는 하나 이상의 개체가 서로를 참조하지만 더 이상 코드를 통해 액세스할 수 없는 경우에 발생합니다.
window.users = null;
null
로 설정해도 참조 계산 알고리즘은 모두 수신 참조를 갖고 있으므로 더 이상 사용되지 않는다고 인식하지 않습니다. Mark and SweepMark and Sweep 알고리즘에는 순환 종속성에 대한 솔루션이 있습니다. 단순히 주어진 객체에 대한 참조를 계산하는 것이 아니라
root
객체에서 액세스할 수 있는지 여부를 감지합니다. 브라우저의
루트
는
window
객체이고, NodeJS의
루트
는
전역
입니다.
dad
개체나
son
개체 모두 루트에서 액세스할 수 없습니다. 따라서 모두 쓰레기로 표시되어 수집됩니다. 이 알고리즘은 2012년 이후 모든 최신 브라우저에서 구현되었습니다. 성능과 구현만 개선되었을 뿐, 알고리즘의 핵심 아이디어는 동일하게 유지됩니다. Trade 자동 가비지 수집을 통해 메모리 관리에 시간을 낭비하는 대신 애플리케이션 구축에 집중할 수 있습니다. 그러나 절충안이 있습니다. 메모리 사용량알고리즘은 메모리가 더 이상 필요하지 않은 시기를 정확히 알지 못하므로 JS 애플리케이션은 실제로 필요한 것보다 더 많은 메모리를 사용할 수 있습니다. 객체가 가비지로 표시되더라도 할당된 메모리를 수집할 시기와 수집 여부를 결정하는 것은 가비지 수집기에 달려 있습니다.
如果你希望应用程序尽可能提高内存效率,那么最好使用低级语言。 但是请记住,这需要权衡取舍。
收集垃圾的算法通常会定期运行以清理未使用的对象。
问题是我们开发人员不知道何时会回收。 收集大量垃圾或频繁收集垃圾可能会影响性能。然而,用户或开发人员通常不会注意到这种影响。
在全局变量中存储数据,最常见内存问题可能是内存泄漏。
在浏览器的 JS 中,如果省略var
,const
或let
,则变量会被加到window
对象中。
users = getUsers();
在严格模式下可以避免这种情况。
除了意外地将变量添加到根目录之外,在许多情况下,我们需要这样来使用全局变量,但是一旦不需要时,要记得手动的把它释放了。
释放它很简单,把null
给它就行了。
window.users = null;
忘记计时器和回调可以使我们的应用程序的内存使用量增加。 特别是在单页应用程序(SPA)中,在动态添加事件侦听器和回调时必须小心。
const object = {}; const intervalId = setInterval(function() { // 这里使用的所有东西都无法收集直到清除`setInterval` doSomething(object); }, 2000);
上面的代码每2秒运行一次该函数。 如果我们的项目中有这样的代码,很有可能不需要一直运行它。
只要setInterval
没有被取消,则其中的引用对象就不会被垃圾回收。
确保在不再需要时清除它。
clearInterval(intervalId);
假设我们向按钮添加了onclick
侦听器,之后该按钮将被删除。旧的浏览器无法收集侦听器,但是如今,这不再是问题。
不过,当我们不再需要事件侦听器时,删除它们仍然是一个好的做法。
const element = document.getElementById('button'); const onClick = () => alert('hi'); element.addEventListener('click', onClick); element.removeEventListener('click', onClick); element.parentNode.removeChild(element);
内存泄漏与前面的内存泄漏类似:它发生在用 JS 存储DOM
元素时。
const elements = []; const element = document.getElementById('button'); elements.push(element); function removeAllElements() { elements.forEach((item) => { document.body.removeChild(document.getElementById(item.id)) }); }
删除这些元素时,我们还需要确保也从数组中删除该元素。否则,将无法收集这些DOM元素。
const elements = []; const element = document.getElementById('button'); elements.push(element); function removeAllElements() { elements.forEach((item, index) => { document.body.removeChild(document.getElementById(item.id)); elements.splice(index, 1); }); }
由于每个DOM元素也保留对其父节点的引用,因此可以防止垃圾收集器收集元素的父元素和子元素。
在本文中,我们总结了 JS 中内存管理的核心概念。写这篇文章可以帮助我们理清一些我们不完全理解的概念。
希望这篇对你有所帮助,我们下期再见,记得三连哦!
原文地址:https://felixgerschau.com/javascript-memory-management/
作者:Ahmad shaded
译文地址:https://segmentfault.com/a/1190000037651993
更多编程相关知识,请访问:编程入门!!
위 내용은 JavaScript의 메모리 관리에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!