관련 무료 학습 권장사항: javascript(동영상)
또 다른 An에 대해 논의하겠습니다. 매일 사용되는 프로그래밍 언어의 성숙도와 복잡성이 증가함에 따라 개발자가 간과하기 쉬운 중요한 주제는 메모리 관리입니다. 또한 JavaScript에서 메모리 누수를 처리하는 방법에 대한 몇 가지 팁을 제공할 것입니다. SessionStack에서 이러한 팁을 따르면 SessionStack이 메모리 누수를 일으키지 않고 통합 웹 애플리케이션의 메모리 소비를 늘리지 않도록 할 수 있습니다.
더 많은 수준 높은 글을 읽고 싶다면 매년 수백 편의 수준 높은 글이 여러분을 기다리고 있습니다!
C와 같은 프로그래밍 언어에는 malloc() 및 free()와 같은 저수준 메모리 관리 기본 요소가 있습니다. 개발자는 이러한 기본 요소를 사용하여 운영 체제에서 메모리를 명시적으로 할당하고 해제합니다.
JavaScript는 객체(객체, 문자열 등)가 생성될 때 메모리를 할당하고 더 이상 사용되지 않으면 메모리를 "자동으로" 해제합니다. 이 프로세스를 가비지 수집이라고 합니다. 이러한 겉보기에 "자동"인 리소스 해제는 JavaScript(및 기타 고급 언어) 개발자에게 메모리 관리에 덜 신경을 쓸 수 있다는 잘못된 인상을 주기 때문에 혼란의 원인이 됩니다.
고급 언어로 작업하는 경우에도 개발자는 메모리 관리를 이해해야 합니다(또는 최소한 기본 사항을 알고 있어야 합니다). 때로는 자동 메모리 관리에 몇 가지 문제(예: 가비지 수집기의 버그 또는 구현 제한 등)가 있으며 개발자는 이러한 문제를 올바르게 처리할 수 있도록 이러한 문제를 이해해야 합니다(또는 최소한으로 유지 관리할 수 있는 적절한 솔루션을 찾아야 합니다). 노력 코드).
어떤 프로그래밍 언어를 사용하든 메모리의 수명주기는 동일합니다.
다음은 메모리 수명주기의 각 단계에 대한 간략한 소개입니다.
JavaScript에서 메모리를 소개하기 전에 메모리가 무엇인지, 어떻게 작동하는지 간략하게 살펴보겠습니다.
하드웨어 수준에서 컴퓨터 메모리는 수많은 트리거에 의해 캐시됩니다. 각 플립플롭에는 1비트를 저장할 수 있는 여러 개의 트랜지스터가 포함되어 있으며, 개별 플립플롭은 고유 식별자로 주소를 지정할 수 있으므로 읽고 덮어쓸 수 있습니다. 따라서 개념적으로 전체 컴퓨터 메모리는 읽고 쓸 수 있는 거대한 배열로 볼 수 있습니다.
인간으로서 우리는 비트 단위로 생각하고 계산하는 데 능숙하지 않기 때문에 비트를 함께 숫자를 나타내는 데 사용할 수 있는 더 큰 그룹으로 구성합니다. 8비트를 1바이트라고 합니다. 바이트 외에도 단어(때로는 16비트, 때로는 32비트)도 있습니다.
많은 것들이 메모리에 저장됩니다:
컴파일러와 운영 체제는 대부분의 메모리 관리를 처리하지만 기본 관리 개념을 더 깊이 이해하려면 기본 상황을 이해해야 합니다.
코드를 컴파일할 때 컴파일러는 기본 데이터 유형을 확인하고 필요한 메모리 양을 미리 계산할 수 있습니다. 그런 다음 필요한 크기가 호출 스택 공간의 프로그램에 할당됩니다. 이러한 변수가 할당되는 공간을 스택 공간이라고 합니다. 함수가 호출되면 해당 메모리가 기존 메모리 위에 추가되고 함수가 종료되면 LIFO(후입선출) 순서로 제거되기 때문입니다. 예:
컴파일러는 필요한 메모리(4 + 4×4 + 8 = 28바이트)를 즉시 인식합니다.
이 코드는 정수 및 배정밀도 부동 소수점 변수가 차지하는 메모리 크기를 보여줍니다. 하지만 약 20년 전에는 정수형 변수가 보통 2바이트를 차지한 반면, 배정밀도 부동 소수점 변수는 4바이트를 차지했습니다. 귀하의 코드는 현재 기본 데이터 유형의 크기에 의존해서는 안됩니다.
컴파일러는 운영 체제와 상호 작용하는 코드를 삽입하고 변수를 저장하는 데 필요한 스택 바이트 수를 할당합니다.
위의 예에서 컴파일러는 각 변수의 정확한 메모리 주소를 알고 있습니다. 실제로 n
변수에 쓸 때마다 내부적으로 "메모리 주소 4127963"
과 같은 정보로 변환됩니다. n
时,它就会在内部被转换成类似“内存地址4127963”
这样的信息。
注意,如果我们尝试访问 x[4]
x[4]
에 액세스하려고 하면 m과 관련된 데이터에 액세스됩니다. 이는 배열에 존재하지 않는 요소(배열에 실제로 할당된 마지막 요소 x[3]보다 4바이트 더 큼)에 액세스하면 일부 m 비트를 읽거나 덮어쓰게 될 수 있기 때문입니다. 이는 프로그램의 나머지 부분에서 확실히 예측할 수 없는 결과를 가져올 것입니다.
함수가 다른 함수를 호출할 때 각 함수는 호출 스택에서 자체 블록을 가져옵니다. 모든 지역 변수를 보유하지만 실행 중 위치를 기억하는 프로그램 카운터도 있습니다. 함수가 완료되면 해당 메모리 블록이 다른 곳에서 다시 사용됩니다. 동적 할당안타깝게도 컴파일 타임에 변수에 필요한 메모리 양을 알 수 없으면 상황이 조금 복잡해집니다. 다음을 수행한다고 가정합니다.
컴파일 시간에 컴파일러는 배열이 사용해야 하는 메모리 양을 알지 못합니다. 이는 사용자가 제공한 값에 따라 결정되기 때문입니다. 따라서 스택에 변수를 위한 공간을 할당할 수 없습니다. 대신 우리 프로그램은 런타임 시 운영 체제에 적절한 공간을 명시적으로 요청해야 하며, 이 메모리는 힙 공간
에서 할당됩니다. 정적 메모리 할당과 동적 메모리 할당의 차이점은 다음 표에 요약되어 있습니다.정적 메모리 할당 | 동적 메모리 할당 |
---|---|
크기는 컴파일 타임에 알려야 합니다. | 크기는 필요하지 않습니다. 컴파일 시간에 알려짐 |
컴파일 시간에 실행 | 런타임에 실행 |
스택에 할당 | 힙에 할당 |
FILO(선입, 후출) | 특정 할당 순서 없음 |
동적 메모리 할당이 어떻게 작동하는지 완전히 이해하려면 포인터에 더 많은 시간을 할애해야 하며, 이는 이 글의 주제에서 너무 벗어날 수 있으므로 여기서는 포인터 관련 지식을 자세히 소개하지 않겠습니다.
이제 첫 번째 단계인 JavaScript에서 메모리를 할당하는 방법을 설명합니다.
JavaScript는 개발자가 수동으로 메모리 할당을 처리해야 하는 책임을 덜어줍니다. JavaScript는 자체적으로 메모리를 할당하고 값을 선언합니다.
특정 함수 호출로 인해 객체의 메모리 할당도 발생합니다.
메서드는 새 값이나 객체를 할당할 수 있습니다.
JavaScript에서 할당된 메모리 사용은 읽기 및 변수나 객체 속성의 값을 읽거나 쓰거나 매개변수를 함수에 전달함으로써 달성할 수 있습니다.
대부분의 메모리 관리 문제는 이 단계에서 발생합니다
여기서 가장 어려운 부분은 할당된 메모리가 더 이상 필요하지 않은 시기를 결정하는 것입니다. 일반적으로 개발자는 메모리가 어디에 있는지 결정해야 합니다. 더 이상 필요하지 않으므로 해제하세요.
고급 언어에는 할당된 내부 조각이 더 이상 필요하지 않은 시점을 감지하기 위해 메모리 할당 및 사용량을 추적하는 작업을 수행하는 가비지 수집기라는 메커니즘이 내장되어 있습니다. 이 경우 자동으로 이 메모리를 해제합니다.
안타깝게도 이 프로세스는 대략적인 추정일 뿐입니다. 특정 메모리 조각이 실제로 필요한지 여부를 알기 어렵기 때문입니다(알고리즘으로 해결할 수 없음).
대부분의 가비지 수집기는 더 이상 액세스되지 않는 메모리를 수집하는 방식으로 작동합니다. 즉, 이를 가리키는 모든 변수가 범위를 벗어났습니다. 그러나 이는 수집할 수 있는 메모리 공간 집합을 과소평가한 것입니다. 메모리 위치의 어느 지점에서든 이를 가리키는 범위 내 변수가 여전히 있을 수 있지만 다시는 액세스되지 않기 때문입니다.
어떤 메모리가 정말 유용한지 판단하는 것이 불가능하기 때문에 가비지 컬렉터는 이 문제를 해결할 수 있는 방법을 생각했습니다. 이 섹션에서는 주요 가비지 수집 알고리즘과 그 제한 사항을 설명하고 이해합니다.
가비지 수집 알고리즘은 주로 참조에 의존합니다.
메모리 관리의 맥락에서, 객체가 다른 객체에 액세스할 수 있는 경우(암시적으로 또는 명시적으로) 다른 객체를 참조한다고 합니다. 예를 들어 JavaScript 개체에는 프로토타입에 대한 참조(암시적 참조)와 속성 값(명시적 참조)이 있습니다.
이 맥락에서 "객체" 개념은 일반 JavaScript 객체보다 더 넓은 범위로 확장되며 함수 범위(또는 전역 어휘 범위)도 포함됩니다.
어휘 범위 지정은 중첩된 함수 내에서 변수 이름이 해결되는 방법을 정의합니다. 내부 함수에는 상위 함수가 반환된 경우에도 상위 함수의 효과가 포함됩니다.
이것은 가장 간단한 가비지 수집 알고리즘입니다. 객체에 대한 참조가 없으면 다음 코드에서와 같이 객체는 "가비지 수집 가능"으로 간주됩니다.
루프에는 제한이 있습니다. 아래 예에서는 서로를 참조하는 두 개의 객체가 생성되어 루프를 생성합니다. 함수 호출이 범위를 벗어나면 사실상 쓸모가 없으며 해제될 수 있습니다. 그러나 참조 계산 알고리즘은 각 개체가 적어도 한 번 참조되었으므로 그 중 어느 것도 가비지 수집될 수 없다고 믿습니다.
이 알고리즘은 개체에 액세스할 수 있는지 여부를 판단하여 개체가 유용한지 여부를 알 수 있습니다. 알고리즘은 다음 단계로 구성됩니다.
이 알고리즘은 이전 알고리즘보다 우수합니다. "개체가 참조되지 않았습니다"는 개체에 액세스할 수 없다는 의미이기 때문입니다.
2012년 현재 모든 최신 브라우저에는 표시 제거 가비지 수집기가 있습니다. 지난 몇 년 동안 JavaScript 가비지 수집(세대/증분/동시/병렬 가비지 수집) 분야에서 이루어진 모든 개선은 가비지 수집 알고리즘 자체의 개선이 아니라 알고리즘(마크-스윕)의 구현 개선이었습니다. 객체에 액세스할 수 있는지 여부를 결정하는 것이 목표입니까?
이 기사에서는 마크 스윕 알고리즘 및 최적화를 포함하여 가비지 수집 추적에 대해 자세히 읽을 수 있습니다.
위의 첫 번째 예에서 함수 호출이 반환된 후 두 개체는 더 이상 전역 개체에서 액세스할 수 있는 개체에 의해 참조되지 않습니다. 따라서 가비지 수집기는 해당 항목에 액세스할 수 없음을 알게 됩니다.
객체 간에 참조가 있지만 루트 노드에서는 도달할 수 없습니다.
가비지 수집기는 편리하지만 고유한 장단점이 있습니다. 그 중 하나는 비결정성입니다. 즉, GC는 예측할 수 없으며 실제로 무엇인지 알 수 없습니다. 가비지 수집이 발생합니다. 이는 어떤 경우에는 프로그램이 실제로 필요한 것보다 더 많은 메모리를 사용한다는 것을 의미합니다. 특히 속도에 민감한 애플리케이션에서는 짧은 일시 정지가 눈에 띌 수 있습니다. 메모리가 할당되지 않으면 대부분의 GC가 유휴 상태가 됩니다. 다음 시나리오를 살펴보세요.
이러한 시나리오에서는 대부분의 GC가 더 이상 수집하지 않습니다. 즉, 컬렉션에 사용할 수 있는 액세스할 수 없는 참조가 있더라도 컬렉터는 해당 참조를 선언하지 않습니다. 이는 엄밀히 말하면 누수는 아니지만 여전히 일반적인 메모리 사용량보다 높은 결과를 가져올 수 있습니다.
기본적으로 메모리 누수는 다음과 같이 정의할 수 있습니다. 애플리케이션에 더 이상 필요하지 않으며 어떤 이유로 운영 체제나 사용 가능한 메모리 풀로 돌아가지 않는 메모리입니다.
프로그래밍 언어는 다양한 메모리 관리 방법을 지원합니다. 그러나 특정 메모리를 사용할지 여부는 실제로 결정되지 않은 질문입니다. 즉, 개발자만이 메모리 조각을 운영 체제에 반환할 수 있는지 여부를 알 수 있습니다.
일부 프로그래밍 언어는 개발자에게 도움을 제공하지만, 다른 프로그래밍 언어는 개발자가 메모리가 더 이상 사용되지 않는 시점을 명확하게 이해하기를 기대합니다. Wikipedia에는 수동 및 자동 메모리 관리에 대한 훌륭한 기사가 있습니다.
JavaScript는 선언되지 않은 변수를 흥미로운 방식으로 처리합니다. 선언되지 않은 변수의 경우 전역 범위에 일치하도록 새 변수가 생성됩니다. 브라우저에서 전역 객체는 window입니다. 예:
function foo(arg) { bar = "some text"; }
는
function foo(arg) { window.bar = "some text"; }
와 동일합니다. bar가 foo 함수 범위 내의 변수를 참조하지만 var를 사용하여 선언하는 것을 잊어버린 경우 예기치 않은 전역 변수가 생성됩니다. 이 예에서 간단한 문자열을 놓치는 것은 큰 해를 끼치지는 않지만 확실히 나쁠 것입니다.
예기치 않은 전역 변수를 생성하는 또 다른 방법은 다음을 사용하는 것입니다:
function foo() { this.var1 = "potential accidental global"; } // Foo自己调用,它指向全局对象(window),而不是未定义。 foo();
JavaScript 파일 시작 부분에 "use strict"를 추가하면 이를 방지할 수 있습니다. 이렇게 하면 실수로 생성되는 것을 방지하기 위해 더 엄격한 JavaScript 구문 분석 모드가 활성화됩니다. 전역 변수.
알 수 없는 전역 변수에 대해 이야기하고 있지만 여전히 명시적인 전역 변수로 채워진 코드가 많이 있습니다. 정의에 따르면 이러한 항목은 수집할 수 없습니다(비어 있거나 재할당되도록 지정하지 않는 한). 많은 양의 정보를 일시적으로 저장하고 처리하는 데 사용되는 전역 변수가 특히 중요합니다. 대량의 데이터를 저장하기 위해 전역 변수를 사용해야 하는 경우 작업을 마친 후 null을 지정하거나 다시 할당해야 합니다.
JavaScript에서 자주 사용되는 setInterval
를 예로 들어 보겠습니다.
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //每五秒会执行一次
위의 코드 조각은 타이머를 사용하여 더 이상 필요하지 않은 노드나 데이터를 참조하는 방법을 보여줍니다.
렌더러가 나타내는 객체는 향후 어느 시점에 삭제되어 내부 핸들러의 전체 코드 블록이 더 이상 필요하지 않게 될 수 있습니다. 그러나 타이머가 여전히 활성 상태이므로 핸들러를 수집할 수 없으며 해당 종속성을 수집할 수 없습니다. 이는 대량의 데이터를 저장하는 serverData를 수집할 수 없음을 의미합니다.
관찰자를 사용할 때는 사용이 끝난 후 이를 삭제하도록 명시적으로 호출해야 합니다(관찰자가 더 이상 필요하지 않거나 개체에 액세스할 수 없게 됩니다).
개발자로서 작업이 끝나면 명시적으로 삭제해야 합니다(그렇지 않으면 개체에 액세스할 수 없게 됩니다).
과거에는 일부 브라우저에서 이러한 상황을 처리할 수 없었습니다(IE6의 경우 좋음). 다행스럽게도 대부분의 최신 브라우저는 이제 이 작업을 수행합니다. 관찰된 객체에 액세스할 수 없게 되면 리스너 제거를 잊어버린 경우에도 자동으로 관찰자 핸들러를 수집합니다. 그러나 객체가 삭제되기 전에 이러한 관찰자를 명시적으로 제거해야 합니다. 예:
오늘날의 브라우저(IE 및 Edge 포함)는 이러한 순환 참조를 즉시 감지하고 처리할 수 있는 최신 가비지 수집 알고리즘을 사용합니다. 즉, 노드가 삭제되기 전에 RemoveEventListener를 호출할 필요가 없습니다.
JQuery와 같은 일부 프레임워크 또는 라이브러리는 노드를 삭제하기 전에(특정 API를 사용하는 경우) 자동으로 리스너를 제거합니다. 이는 IE 6과 같은 문제가 있는 브라우저에서 실행되는 경우에도 메모리 누수가 발생하지 않도록 보장하는 라이브러리 내의 메커니즘에 의해 구현됩니다.
클로저는 내부 함수가 외부(포함된) 함수의 변수를 사용하는 JavaScript 개발의 핵심 측면입니다. JavaScript 실행 방법에 대한 세부 정보로 인해 다음과 같은 방식으로 메모리 누수가 발생할 수 있습니다.
이 코드는 한 가지 작업을 수행합니다. replaceThing
이 호출될 때마다 theThing 코드 >는 큰 배열과 새로운 클로저(someMethod)를 포함하는 새로운 객체를 얻습니다. 동시에 <code>unuse
d 변수는 `originalThing
을 참조하는 클로저를 가리킵니다. replaceThing
的时候,theThing
都会得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量unuse
d指向一个引用了`originalThing
的闭包。
是不是有点困惑了? 重要的是,一旦具有相同父作用域的多个闭包的作用域被创建,则这个作用域就可以被共享。
在这种情况下,为闭包someMethod
而创建的作用域可以被unused
共享的。unused
内部存在一个对originalThing
的引用。即使unused
从未使用过,someMethod
也可以在replaceThing
的作用域之外(例如在全局范围内)通过theThing
来被调用。
由于someMethod
共享了unused
闭包的作用域,那么unused
引用包含的originalThing
会迫使它保持活动状态(两个闭包之间的整个共享作用域)。这阻止了它被收集。
当这段代码重复运行时,可以观察到内存使用在稳定增长,当GC
运行后,内存使用也不会变小。从本质上说,在运行过程中创建了一个闭包链表(它的根是以变量theThing
someMethod
클로저에 대해 생성된 범위는 unused
에서 공유할 수 있습니다. unused
내에 originalThing
에 대한 내부 참조가 있습니다. unused
가 전혀 사용되지 않더라도 someMethod
는 replaceThing
범위 외부의 theThing에 전달될 수 있습니다(예: 전역 범위). )
를 호출합니다. someMethod
는 unused
클로저의 범위를 공유하므로 포함된 originalThing
에 대한 unused
의 참조는 강제로 활성 상태를 유지합니다(두 클로저 사이의 전체 공유 범위). 이렇게 하면 수집이 방지됩니다. 이 코드를 반복적으로 실행하면 메모리 사용량이 꾸준히 증가하는 것을 확인할 수 있습니다. GC
를 실행해도 메모리 사용량은 줄어들지 않습니다. 기본적으로 클로저의 연결된 목록은 런타임 중에 생성되고(해당 루트는 theThing
변수의 형태로 존재함) 각 클로저의 범위가 큰 배열을 간접적으로 참조하므로 상당한 메모리 누수가 발생합니다.
4. DOM에서 참조 분리
때로는 DOM 노드를 데이터 구조에 저장하는 것이 유용할 수 있습니다. 테이블의 여러 행을 빠르게 업데이트하려는 경우 각 DOM 행에 대한 참조를 사전이나 배열에 저장할 수 있습니다. 이러한 방식으로 동일한 DOM 요소에 대한 두 개의 참조가 있습니다. 하나는 DOM 트리에 있고 다른 하나는 사전에 있습니다. 나중에 이 행을 삭제하기로 결정한 경우 두 참조에 모두 액세스할 수 없도록 설정해야 합니다. 🎜🎜DOM 트리에서 내부 노드나 리프 노드를 참조할 때 고려해야 할 또 다른 문제가 있습니다. 코드에서 테이블 셀(위 내용은 JavaScript 메모리 관리 소개 + 4가지 일반적인 메모리 누수 처리 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!