[자바] 메모리 관리

자바 메모리 관리

JVM도 다른 소프트웨어와 마찬가지로 호스트 운영체제의 가용 메모리를 할당받는다.

JVM 내부적으로 다음과 같이 분리된 메모리 공간이 존재한다.

  1. 힙 메모리
  2. 스택 메모리
  3. 캐시 메모리


소스 코드 실행 시 변수 및 객체 선언, 메소드 호출 등 여러 동작들의 수행 과정에서 여러 데이터들이 JVM의 메모리 공간에 저장된다.


힙 메모리

힙(heap) 메모리는 동적 메모리 할당(dynamic memory allocation)이 일어나는 메모리 영역이다. 객체 타입의 데이터가 저장되는 영역이며 가비지 컬렉터에 의해 관리되는 영역이다. 클래스 영역에 선언된 인스턴스 변수, 클래스(정적) 변수가 힙 메모리에 저장된다. 배열은 객체 타입이며 따라서 힙 메모리에 저장된다. byte, int, boolean과 같은 원시(primivite) 타입의 데이터가 인스턴스 또는 클래스 변수로 선언된다면 힙 메모리에 저장된다.

모든 스레드는 힙 메모리 영역을 공유하여 객체에 접근할 수 있으므로 힙 메모리는 스레드 안전하지 않다. 따라서 적절한 객체 잠금(lock) 및 동기화(synchronization) 프로세스가 별도로 필요하다.

힙 메모리가 가득 차서 더 이상 메모리 할당이 불가능하면 JVM은 OutOfMemoryException 예외를 발생시킨다.


자동 가비지 컬렉션

자동 가비지 컬렉션(automatic garbage collection)이란 힙 메모리를 탐색하여 어떤 객체가 사용 중이고 어떤 것이 미사용 중인지 확인한 후 미사용 객체를 삭제하는 프로세스이다.

객체가 사용 중이라는 것은(또는 객체가 참조되고 있다는 것은) 프로그램의 일부가 객체에 대한 포인터(참조)를 유지하고 있다는 것을 의미한다. 반면 객체가 사용되지 않고 있다는 것은 프로그램의 어느 부분에서도 해당 객체가 더 이상 참조되지 않는다는 것을 의미한다. 참조되지 않는 객체가 사용하는 메모리는 회수할 수 있다.

C 언어에서 메모리 할당 및 할당 해제(미사용 객체 삭제) 프로세스는 수동 프로세스이지만 자바 언어에서 이 프로세스는 가비지 컬렉터에 의해 자동으로 처리된다.

가비지 컬렉터에 의한 메모리 할당 해제 프로세스는 다음과 같다.

  1. 마킹 (marking)
  2. 일반 삭제 (normal deletion)
  3. 압축 삭제 (deletion with compacting)


프로그램 실행 중 새로 생성되는 객체들은 대부분 수명이 짧으며 따라서 시간이 지날수록 메모리에 할당된 상태로 남아있는 객체들의 개수가 적어짐을 알 수 있다. 즉, 대부분의 객체들은 수명이 매우 짧다. 이러한 시간에 따른 객체들의 메모리 할당 특성을 이용하여 JVM의 성능을 향상시킬 수 있다. 힙 메모리를 쪼개서 더 작은 부분(또는 제너레이션)으로 나누는 것이다.

힙 메모리는 영(young) 제너레이션, 올드(old) 제너레이션, 퍼머넌트(permanent) 제너레이션으로 구성된다. 각 제너레이션은 시간에 따른 객체들의 메모리 할당 특성에 따라 구분된 것이다. 가비지 컬렉션 과정 동안 객체들이 시간 순으로 힙 메모리의 각 제너레이션을 거치게 된다.


힙 메모리의 영역

  1. 영 제너레이션

    영(young) 제너레이션 영역은 새로 생성된 객체가 할당되는 메모리 영역이다. 영 제너레이션이 채워지면 마이너(minor) 가비지 컬렉션이 일어난다. 영 제너레이션에 있는 수명을 다한 객체들은 빠르게 수집되고 살아남아 있는 객체들은 올드 제너레이션으로 이동하게 된다.

    모든 마이너 가비지 컬렉션은 ‘Stop the World’ 이벤트이다. 이는 마이너 가비지 컬렉션 동작이 진행되는 동안 모든 애플리케이션 스레드가 중지됨을 의미한다.


  1. 올드 제너레이션

    올드(old) 제너레이션 영역은 오랜기간 살아남아 있는 객체들을 저장하는데 사용되는 힙 메모리 영역이다. 테니어드(tenured) 제너레이션이라고도 한다. 영 제너레이션 객체들에 대해 기준(객체의 수명)이 설정되고 시간이 지남에 따라 이 기준에 해당하는 객체들은 올드 제너레이션으로 이동하게 된다.

    최종적으로 올드 제너레이션 객체들도 수집되어야 하며 이러한 이벤트를 메이저(major) 가비지 컬렉션이라고 한다.


  1. 퍼머넌트 제너레이션

    퍼머넌트(permanent) 제너레이션 영역은 애플리케이션이 사용하는 런타임 클래스와 메소드들을 나타내기 위해 JVM이 필요로 하는 메타데이터를 담고 있는 메모리 영역이다. 퍼머넌트 제너레이션은 애플리케이션이 사용 중인 클래스를 기반으로 런타임 시에 JVM에 의해 채워진다. 자바 라이브러리 클래스와 메소드들도 이 메모리 영역에 저장된다.

    JVM은 더 이상 필요하지 않은 클래스를 발견한 후 다른 클래스들을 위해 메모리 공간이 필요해지면 미사용 클래스를 수집한다(메모리 할당이 해제된다).


스택 메모리

스택(stack) 메모리는 정적 메모리 할당(static memory allocation)이 일어나는 메모리 영역이다. 스택 메모리는 스레드 실행에 사용되며 각 스레드마다 하나씩 존재한다. 스레드 시작 시 스택 메모리가 할당된다.

스택 메모리는 메서드에 속하는 로컬 원시(local primitive) 값과 객체가 저장된 메모리의 주소인 객체의 참조(reference)가 존재하는 메모리 영역이다. 즉, 스택 메모리에는 로컬 원시 변수 및 로컬 참조 변수가 저장된다. 새로 생성된 객체는 힙 메모리 영역에 저장되고 해당 객체에 대한 참조는 스택 메모리 영역에 저장된다.

새로운 메서드(또는 함수)가 실행되고 종료됨에 따라 스택 메모리가 증가 및 감소한다. 메서드가 생성하는 값은 메서드가 실행 중인 동안에만 스택 메모리에 존재한다.

스택 메모리에 저장된 데이터에는 후입선출(LIFO) 방식으로 접근할 수 있다. 이름 그대로 스택 자료 구조를 사용하여 데이터를 처리한다. 메서드가 호출되면 프레임(frame)이라는 데이터 저장 구조가 생성되고, 메서드 호출이 완료되면 프레임은 제거된다. 메서드 호출 시 하나의 메서드 당 하나의 프레임이 생성되며 프레임에는 각각의 메서드에 대한 데이터들이 담겨 있다. 메서드 호출 정보가 프레임에 담기고 이 프레임이 스택 메모리에 저장된다.

메서드 호출 시 메서드의 정보가 담긴 프레임을 스택 구조에 저장(push)하고 메서드 실행 종료 시 프레임을 스택 구조에서 제거(pop)한다. 새로운 메서드 호출 시 기존 메서드 호출 정보가 스택 구조에 저장됨으로써 가장 마지막에 호출된 메서드를 먼저 실행한 이후 바로 이전에 호출된 메서드를 계속해서 실행해 나갈 수 있다.

자바에서 각각의 스레드는 자신만의 스택 메모리 영역을 가지고 있으며 각 스레드들이 병렬적으로 호출하는 메서드들의 호출 정보는 각 프레임에 저장된다. 스레드 별로 스택 메모리 영역이 분리되어 사용되므로 스택 메모리 영역은 힙 메모리와 달리 스레드 안전하다.

스택 메모리에 저장되는 메서드 영역의 지역 변수들은 해당 메서드 호출이 종료되는 순간 메모리에서 제거된다. 메서드가 호출되면 스택 메모리가 할당되고 메서드가 종료되면 스택 메모리 할당이 해제된다.

스택 메모리가 가득차서 더 이상 메모리 할당이 불가능하면 JVM은 StackOverFlowException 예외를 발생시킨다. 재귀적으로 메서드를 호출하는 경우 재귀의 깊이가 메모리 제한을 넘어서게 된다면 해당 예외가 발생할 수 있다.


힙 메모리 vs. 스택 메모리

  • 객체는 힙 메모리에 저장되며 객체의 참조(객체가 저장된 메모리의 주소)는 스택 메모리에 저장된다.
  • 메서드 영역에 선언된 로컬 변수, 객체의 참조 변수는 스택 메모리에 저장된다. 클래스 영역에 선언된 인스턴스 변수, 클래스(정적) 변수는 힙 메모리에 저장된다.
  • 스택 메모리는 메서드가 실행 중인 동안에만 존재하며 힙 메모리는 애플리케이션이 실행 중인 동안에 존재한다.
  • 하나의 스레드 당 하나의 스택 메모리를 사용하지만 힙 메모리는 애플리케이션 전체에서 여러 스레드들이 공유하여 사용한다.
  • 스택 메모리는 스레드 안전하지만 힙 메모리는 스레드 안전하지 않다.
  • 스택 메모리 영역에서는 메소드의 호출 및 실행 종료 순으로 메모리 할당 및 할당 해제가 일어남으로써 메모리의 관리가 자동적으로 수행되는 반면 힙 메모리 영역에서는 시간에 따른 미사용 객체에 대한 메모리 할당 해제가 일어나며 따라서 메모리의 관리가 보다 복잡하다.
  • 스택 메모리 영역에 대한 접근은 힙 메모리 영역에 비해 상대적으로 빠르게 수행된다.


래퍼 클래스와 메모리

래퍼(wrapper) 클래스란 byte, int, boolean과 같은 원시 타입에 대한 객체 타입을 제공하는 클래스를 말한다. 래퍼 클래스는 원시 타입의 데이터를 저장하는데 사용하는 객체 타입 변수이므로 힙 메모리에 저장된다.

기본적으로 래퍼 클래스는 불변(immutable)이다. 한 번 인스턴스화를 한 이후에는 값을 변경할 수 없다. 자바는 java.util.concurrent.atomic 패키지를 통해 Atomic으로 시작하는 가변(mutalble) 래퍼 클래스를 제공한다. 이 클래스는 인스턴스의 값을 원자적(atomic)으로 변경할 수 있도록 한다.


캐시 메모리 (코드 캐시)

JVM은 네이티브 코드를 생성한 후 이를 코드 캐시(codecache)라는 메모리 영역에 저장한다. JIT(Just-in-Time) 컴파일러는 코드 캐시 영역의 가장 큰 소비자이며 이로 인해 JIT 코드 캐시라고 부르기도 한다.

코드 캐시는 크기가 고정되어 있으며 코드 캐시 메모리가 가득차게 되면 JIT 컴파일러가 비활성화되어 JVM은 추가적으로 코드를 컴파일하지 않게 되며 이는 성능 저하를 유발한다. 즉, 코드 캐시 메모리가 부족하면 JVM이 JIT 컴파일러를 비활성화할 수 있으며, 이는 성능에 상당한 영향을 미칠 수 있다.

성능을 유지하면서 JIT 컴파일러의 코드 캐시 사용량을 줄이는 방법이 존재한다.


참고

Comments