ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 브라우저는 메모리를 어떻게 관리하길래 크롬 탭 하나가 700MB씩 먹는걸까
    개발/자바스크립트로의 깊은 잠수 2026. 3. 4. 21:32

    크롬 탭 위에 마우스를 올려보면 메모리 사용량을 알려줍니다.

    별거 없는 페이지라고 생각했는데 700MB씩 잡아먹는 모습을 보면 문득 이게 700MB씩 먹을 페이지인가? 싶은 의문이 드는 경우가 종종 있었습니다.

    그 이유를 찾아봤습니다.

    얘는 심지어 1GB를 넘겼습니다. 얘 요즘 램값이 비싸단다. 아껴써야 해

    V8 엔진이 범인인가?

    크롬 브라우저는 V8 엔진이라는 자바스크립트 엔진으로 JS 코드를 컴파일하고 실행합니다.

    어쩌면 V8 엔진이 메모리를 좀 희생해서 속도를 얻어내는 건 아닐까요?

    보통 속도와 메모리 사용량 사이에는 trade-off 관계가 있으니까 말이에요.

     

    이런 합리적 의심을 바탕으로, V8이 메모리를 관리하는 방식을 한번 살펴보겠습니다.

     

     

    위의 그림은 V8 엔진의 메모리 구조를 도식화한 그림입니다.

    모든 JS 프로그램은 V8 프로세스에 속하는 일련의 메모리 영역들로 대변될 수 있겠습니다.

    이 메모리 영역들을 Resident Set 이라고 부릅니다.

    직역하면 거주자 집합이라는 뜻이네요. 프로세스 나라에는 이런 메모리 영역들이 거주하고 있답니다.

     

    메모리 영역은 크게 보면 힙 영역과 스택 영역으로 나눌 수 있습니다.

     

    스택 영역에는 정적 데이터들이 들어갑니다. 함수 호출 프레임, 원시값, 객체 포인터 같은 데이터들이죠.

    이 친구들은 상대적으로 아주 적은 메모리를 차지합니다. 보통 500KB에서 많아봐야 2MB 이하입니다.

    무시할 수 있는 수준이라고 봐도 되겠습니다.

     

    반면에 힙 영역은 굉장히 커질 수 있습니다. 객체와 동적 데이터들이 위치하는 영역인데요. 공간별로 쪼개서 보면 다음과 같습니다.

    • New Space(Young Generation)는 기본적으로 새로운 객체가 만들어졌을 때 처음에 들어가는 공간입니다.
    • Old Space(Old Generation)는 New Space에서 두 번의 마이너 가비지 컬렉션 이후에 살아남은 객체들이 이동하는 공간입니다.
    • Large Object Space는 size limit을 초과하는 대형 객체들이 들어가는 공간입니다.
    • Code Space는 JIT 컴파일러가 컴파일한 코드블록이 들어가는 공간입니다.
    • Cell Space, Property Cell Space, Map Space는 각각 `Cells`, `PropertyCells`, `Maps`가 들어가는 공간입니다.

     

    아래 링크에 들어가면 그림을 통해 실제 코드가 실행되는 방식, 어떻게 스택과 힙 메모리가 사용되는지를 볼 수 있습니다.

    https://speakerdeck.com/deepu105/v8-memory-usage-stack-and-heap

     

    이런 식으로 메모리가 할당된다는 사실은 이제 알겠는데, 어떻게 관리되는지도 좀 더 알아볼까요?

    V8 엔진이 메모리 먹는 범인인지를 가려내기 위해서는 필요 없어진 메모리를 잘 비워주는지도 알아봐야겠죠.

     

    필요 없어진 메모리인지 확인하고 비워주는 행위를 GC(Garbage Collection)라고 부릅니다.

    V8은 Minor GC, Major GC를 진행합니다.

     

    Minor GC는 New Space를 관리합니다.

    V8은 새로운 객체를 위한 공간이 필요해지면 할당 포인터라는 것을 증가시킵니다.

    이 포인터가 가리키는 공간이 New Space의 끝을 넘어서게 되면 Minor GC가 작동합니다. 그리고 더이상 참조되지 않는 객체들을 찾아내서 그 공간을 해제시킵니다(비워줍니다).

    이 과정은 Scavenger라고도 불리며 Cheney's algorithm을 구현합니다.

    Minor GC는 stop-the-world 방식으로 프로그램 동작 전체를 멈추게 하지만, 굉장히 빠르고 효율적으로 이루어지기 때문에 무시할 수 있을 정도의 시간을 소비합니다.

     

    Major GC는 Old Space를 관리하는 과정입니다.

    V8 엔진은 동적으로 메모리 한계를 계산해서, 이를 토대로 Old Space가 부족하다고 판단하면 Major GC를 동작시킵니다.

    위에서 살펴본 Scavenger 알고리즘은 작은 공간을 관리하는 데는 적합하지만, 계산 과정에서 메모리를 좀 사용하기 때문에 (메모리 오버헤드가 있기 때문에) Old Space같은 큰 공간을 관리하기에는 부적합합니다.

    그래서 Major GC는 Mark-Sweep-Compact 알고리즘을 사용합니다.

     

     

    Major GC도 stop-the-world 방식으로 동작하기 때문에 프로그램 실행을 멈춥니다.

    프로그램이 너무 오래 멈추는 것을 방지하기 위해서, V8은 몇 가지 최적화된 방식을 사용합니다.

    • 점진적 GC : 한번에 모든 걸 하지 않고 단계별로 진행합니다.
    • 동시 Marking/Sweeping/Compacting : 메인 JavaScript 스레드를 건드리지 않는 여러 helper 스레드가 동시에 돌아가면서 Mark-Sweep-Compact 과정을 진행합니다.
    • Lazy Sweeping : Sweeping을 미루고 있다가, 메모리가 실제로 필요한 시점에 동작합니다.

     

    아하! 여기 뭔가 단서가 있네요.

    프로그램 성능을 위해서 GC를 미루는, Lazy Sweeping 기술이 메모리를 잡아먹는 범인 중 하나인 것입니다.

     

     

     

    메모리를 희생해서 속도를 높인다

    찾아보니 Lazy Sweeping 말고도 메모리 사용량이 높아질 수 있는 다른 원인이 있었습니다.

     

    V8 엔진은 GC를 통해 힙 메모리 자원을 해제한 이후에도, 해제된 메모리를 곧바로 운영체제에 반환하지 않습니다.

    대신 재사용을 위한 메모리 공간으로 확보해 놓습니다.

     

    이렇게 설계한 이유는 뭘까요?

     

    우선 메모리 재할당 비용이 크기 때문입니다. 운영체제가 관리하는 넓은 메모리 영역에서 메모리를 떼오고 반환하는 행위는 오버헤드가 있죠. 반면 V8 엔진이 관리하는 프로세스 레벨에서 한번 떼온 메모리를 계속 관리한다면 상대적으로 적은 비용으로 메모리를 재할당할 수 있어서 비용이 절감됩니다.

     

    그리고 한번 메모리가 쓰였다면, 다시 메모리가 필요해질 가능성이 높기 때문입니다.

    어떤 페이지가 메모리를 많이 쓰는 이유는, 메모리를 많이 쓰는 로직을 담고 있기 때문일 겁니다.

    뭔가 당연한 말이죠?

    왜냐하면 변화하기 위해서는 바뀌어야 하기 때문이다

     

    메모리도 써본 놈이 잘 쓴다고, 그 페이지는 앞으로도 메모리를 많이 요구할 수 있기 때문에 일단 당장 해제된 메모리라고 하더라도 반환하지 않는 겁니다.

    그러면 다음에 또 필요해졌을 때 좀 더 빠르게 메모리를 재할당할 수 있게 됩니다.

     

    V8 엔진은 메모리를 넉넉하게 쓰고, 그만큼의 성능을 확보하는 전략을 채택했다고 볼 수 있겠네요.

     

     

     

    개발자의 잘못은 없는가?

    사실 크롬 탭이 메모리를 많이 먹는 근본적인 이유는, 정말로 메모리가 많이 필요하기 때문입니다.

    HTML DOM 구조 자체가 거대할 수도 있고, 거대한 객체 참조가 필수적으로 유지될 필요가 있을 수도 있습니다.

    또 이미지, 동영상이 페이지에 많을 수도 있습니다.

     

    개발자가 의도적으로 페이지에 많은 것을 담아 놓았다면 당연히 메모리 소비량이 클 수밖에 없죠.

    정말 필요한 것들이라면 어쩔 수 없겠지만, 필요하지 않은 것들이 있는지에 대한 점검이 필요하긴 합니다.

     

    예를 들어 무한 스크롤로 콘텐츠를 계속 추가하는 페이지가 있다면, 기존 콘텐츠들을 유지하면서 계속 새로운 콘텐츠들을 아래쪽에 추가하게 될 겁니다.

    스크롤이 길어지면 길어질수록 메모리를 많이 잡아먹겠죠.

    이러면 DOM 노드 수가 증가하고, 이미지/비디오 같은 리소스도 누적되고, 데이터 배열도 커지고, 제대로 관리하지 않는다면 이벤트리스너도 누수될 수 있습니다.

    최적화를 위해서는 화면에 보이는 구간만 렌더링하도록 가상화(virtualization)하거나, 이미지 태그에 `loading="lazy" decoding="async"` 같은 메모리 최적화 속성을 부여하는 등의 방법이 있겠습니다.

     

     


     

    현대에 와서 메모리는 값싼 자원이 되었고, 그만큼 메모리를 희생하고 성능을 얻는 전략을 많이 사용합니다.

    근데 요즘에는 좀 과하다는 생각이 들긴 합니다.

    많은 프로그램들이 실제로 사용하지도 않으면서 RAM을 너무 과하게 차지하고 있어요.

     

    프로세스가 메모리를 꽉 잡고 놓아주지 않으면, 운영체제는 부족한 공간에 대해서는 속절없이 스왑 공간을 사용할 수밖에 없습니다. 그러면 하드 디스크에 메모리 컨텍스트를 읽고 쓰는 비싼 비용을 치루게 되기 때문에 오히려 성능이 더 떨어지죠.

     

    아낄 수 있는 메모리는 아끼는 개발 문화(?)가 형성되기를 바라 봅니다.

     

     

     

    References

     

     

     

     

    댓글

Copyright 2022. ProdYou All rights reserved.