JVM이란?

JVM(Java Virtual Machine)은 자바 가상 머신으로, 실제 운영체제 대신 자바 프로그램을 실행시켜주는 대리 역할을 한다.
자바 프로그램은 운영체제의 종류와 상관없이 실행이 가능한데, 이는 JVM이 자바 코드를 컴파일 해 얻은 바이트 코드를 운영체제가 이해할 수 있는 기계어로 바꿔 대신 실행시켜주는 역할을 하기 때문이다.

자바 프로그램이 실행되는 개략적인 구조를 보면 아래와 같다.

java_program

  • Java Source: 사용자가 작성한 Java 코드
  • Java Compiler: Java Source 파일을 JVM이 해석할 수 있는 Java Byte Code로 변경
  • Class file: Java Compiler에 의해 변환된 Byte Code (.class 파일)
  • Class Loader: JVM 내로 Byte Code를 로딩해 클래스들을 Runtime Data Area에 배치
  • Execution Engine: 로딩된 클래스의 Byte Code를 해석(Interpret)
  • Garbage Collector: Heap 메모리 영역에 적재된 객체 중 참조되지 않은 객체들을 탐색하고 제거
  • Runtime Data Area: JVM이 프로그램을 수행하기 위해 운영체제에서 할당받은 메모리 영역

JVM에서 Class Loader를 통해 Class 파일들을 로딩하고, 로딩된 Class 파일은 Execute Engine을 통해 해석된다.
해석된 프로그램은 Runtime Data Area에 적재돼 실질적인 수행이 이뤄지게 된다.

JVM의 내부 구조를 더 자세히 보면 아래와 같다.

jvm

  • Method 영역: 클래스, 변수, Method, static 변수, 상수 정보 등이 저장되는 영역(모든 Thread가 공유)
  • Heap 영역: new 명령어로 생성된 인스턴스와 객체가 저장되는 영역(모든 Thread가 공유)
  • Stack 영역: Method 내에 사용되는 값이 저장되는 영역, 메소드가 호출될 때 적재되고, 실행이 완료되면 LIFO로 삭제(각 Thread별로 생성)
  • PC Register: 현재 수행중인 JVM 명령의 주소값 저장(각 Thread별로 생성)
  • Native Method Stack: 다른 언어(C/C++ 등)의 메소드 호출을 위해 할당되는 영역, 언어에 맞게 Stack이 생성됨

이 중에서 Garbage Collector의 주요 대상인 Heap에 대해서 자세히 살펴보도록 한다.

Heap 과 Garbage Collector

Heap은 위 그림에서 볼 수 있듯이 Eden, Survivor가 있는 Young 영역, Old 영역, Permanent영역으로 나뉘어있다.

여기서 Permanent 영역은 JDK 7까지 존재하고, JDK 8에서는 Native Method Stack으로 이동해 Metaspace 영역으로 옮겨졌다.

이 영역은 Class의 Meta 정보나 Method의 Meta 정보, Static 변수와 상수가 저장되는 공간으로, 메타 데이터 저장 영역이라고도 한다.

Static 변수와 상수가 저장되는 구간은 Metaspace 영역이 아닌 Heap 영역에 저장되도록 바뀌었는데, 이 영역에 Static 변수들이 쌓여 영역이 가득차게 되면 `OOM(Out Of Memory)`이 발생하기 때문에 최대한 GC의 대상이 될 수 있게 하기 위해서 바뀌었다고 한다.

Permanent 영역에서도 GC의 대상이 안됐는데, Heap 영역이라고 해서 GC의 대상이 될 수 있을까?

Heap에서도 물론 Static 변수들은 잘 삭제되지 않는다. 때문에 Heap을 튜닝해 몇 일에 한번씩 Heap을 초기화 하고 재생성하도록하여 최대한 Static 변수가 쌓이지 않도록 조절한다.

이렇게 영역이 나뉨으로써 GC가 더 효율적으로 일어날 수 있다.
GC는 동작 영역에 따라 Minor GC와 Major GC로 나뉘는데, 아래에서 그 과정을 확인하자.

Minor GC: Young 영역에서 일어나는 GC

minor_gc

  1. 최초 객체는 Eden 영역에 생성된다.
  2. Eden 영역에 객체가 가득(또는 기준치 이상)차면 참조되지 않은 객체들을 삭제한다. (GC)
  3. 삭제하고 남은 객체들을 Survivor0 영역으로 옮긴다.
  4. 이를 반복하다가 Survivor0가 가득차게되면 Eden과 Survivor0에서 참조되지 않은 객체들을 삭제한다.
  5. 삭제하고 남은 객체들을 Survivor1 영역으로 옮긴다.
  6. 이 과정 중 Survivor 영역에서 일정 횟수 이상 살아남은 객체를 Old 영역으로 옮긴다.
  7. 이 과정을 계속해서 반복한다.

여기서 Old 영역이 가득(또는 기준치 이상)차게 되면 아래의 Full GC가 발생한다.

Major GC(Full GC): Old 영역에서 일어나는 GC

  1. Old 영역에서 참조되지 않은 객체들을 검사해 한번에 삭제한다.

이는 Minor GC 보다 시간이 훨씬 많이 걸리고, 실행 중에 GC를 제외한 모든 Thread를 일시정지 시킨다.
Full GC는 한번에 객체를 삭제하면서 Heap 영역에 빈 메모리 공간들이 생겨나는데, 이를 없애기 위해서 메모리 전체를 재구성한다.
이 과정에서 다른 Thread가 메모리를 사용하면 안되기 때문에 모든 Thread를 일시 정지 시키게 되는 것이다.
만약 Full GC가 오랫동안 일어나게 되면, 수 초동안 모든 Thread가 정지해 시스템 장애가 발생하는 문제점이 생길 수 있다.

더 자세한 과정과 내용을 알고싶다면, 여기를 참고하면 된다.

OOM(Out of Memory)

앞서 Permanent 영역을 설명하면서, OOM에 대해 언급했다.
OOM은 Out of Memory의 약자로 할당된 메모리의 공간을 모두 사용하여, 새로 메모리 공간을 할당할 여유가 없다는 것을 뜻한다.

OOM은 잘 코딩된 프로그램이라면 잘 발생하지 않지만, 메모리 누수가 발생하거나 프로그램에 Static 변수가 너무 많을 경우 주로 발생한다.
특히 24시간 서비스하는 웹 서버와 같은 경우에는 메모리 누수가 발생하지 않는지 항상 확인해야 할 필요가 있다.

OOM이 발생할 경우 jmap, jhat을 이용해 힙 덤프를 확인해 원인 분석 후 수정하도록 한다.