본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[JVM Internal] JVM 메모리 구조

JVM이란?



JVM(Java Virtual Machine)을 어떻게 정의할 것인가 고민하기에 앞서 기본적 JVM 원어로부터 알아보겠습니다. JVM은 물리적인 형태가 아닌 소프트웨어로서 하나의 개념으로 존재합니다. 이것이 "VIRTUAL"을 뜻하는 것입니다. 그리고 JVM은 독자적으로 작동할 수 있는 메커니즘과 구조를 가지고 있습니다. 이는 하나의 축약된 컴퓨터와 같은 의미에서 "MACHINE"이라는 단어가 조합된 것입니다.


그러나 "JVM"이 구체적으로 무엇이냐는 질문에 정확한 정의를 내린다기 보다는, 하나의 개념, 스펙(Specification)에 지나지 않는 즉, 어느 누구도 정확하고 자세한 설계도를 제공하지 않고 단지 이렇게 저렇게 해야 한다는 식의 표준화된 정의만으로 존재할 뿐입니다.


이러한 표준화된 정의가 나오면 각 JVM 벤더들(Oracle, IBM 등)은 이러한 표준에 맞도록 자신들의 JVM을 별도로 구현하여 사용하곤 합니다.


결국 JVM은 정의된 스펙을 구현한 하나의 독자적인 프로세스 형태로 구동되는 Runtime Instance라고 할 수 있습니다. 따라서 JVM의 역할은 개발자들이 작성한 Java 프로그램이나 Web 애플리케이션 Server(WAS) 등을 구별하지 않고 Java 프로그램의 범주에 들어가는 모든 것들을 실행시키는 기본 데몬을 JVM이라고 칭합니다.


Java에서 프로그램을 실행한다는 것은 컴파일 과정을 통하여 생성된 Class 파일을 JVM으로 로딩하고 ByteCode를 해석(interpret)하는 과정을 거쳐 메모리 등의 리소스를 할당하고 관리하며 정보를 처리하는 일련의 작업들을 포괄합니다. 이 때 JVM은 Thread 관리 및 Garbage Collection과 같은 메모리 정리 작업도 수행하게 됩니다.




 - Java Source: 사용자가 작성한 Java 코드이다

 - Java Compiler: Java Source 파일을 JVM이 해석할 수 있는 Java Byte Code로 변경한다.

 - Java Byte Code: Java Compiler에 의해 수행될 결과물이다(확장자 .class 파일)


 - Class Loader: JVM 내로 .class 파일들을 Load하여 Loading된 클래스들을 Runtime Data Area에 배치된다.

 - Execution Engine: Loading된 클래스의 Bytecode를 해석(interpret)한다.

 - Runtime Data Area: JVM이라는 프로세스가 프로그램을 수행하기 위해 OS에서 할당받은 메모리 공간이다.


JVM상에서 Class Loader를 통해 Class 파일들을 로딩시키고, 로딩된 Class 파일들은 Execute Engine을 통해 해석됩니다. 해석된 프로그램은 Runtime Data Areas에 배치되어 실질적인 수행이 이루어지게 됩니다. 이러한 JVM의 내부 구조를 좀 더 도식화하면 아래와 같습니다. 그 중에서 Runtime Data Area의 내부에 대해서 좀 더 자세히 알아보겠습니다.




 - Method Area: 클래스, 변수, Method, static변수, 상수 정보 등이 저장되는 영역(모든 Thread가 공유)

 - Heap Area: new 명령어로 생성된 인스턴스와 객체가 저장되는 구역(Garbage Collection 이슈는 이 영역에서 일어나며, 모든 Thread가 공유)

 - Stack Area: Method 내에서 사용되는 값들(매개변수, 지역변수, 리턴값 등)이 저장되는 구역으로 메소드가 호출될때 LIFO로 하나씩 생성되고, 메소드 실행이 완료되면 LIFO로 하나씩 지워진다. (각 Thread별로 하나씩 생성)

 - PC Register: CPU의 Register와 역할이 비슷하다. 현재 수행중인 JVM 명령의 주소값이 저장된다. (각 Thread별로 하나씩 생성)

 - Native Method Stack: 다른 언어(C/C++ 등)의 메소드 호출을 위해 할당되는 구역으로 언어에 맞게 Stack이 형성되는 구역이다.


이러한 실행 과정 속에서 JVM은 필요에 따라 Thread Synchronization과 Garbage Collection과 같은 관리 작업을 수행하게 됩니다. 이러한 수행 작업에 대해 이후부터 좀 더 자세히 다루고자 하며, 그 전에 JVM Heap 메모리 구조와 Garbage Collection에 대해 언급하고자 합니다.



Java Heap


Runtime Data Area 중에 여러분들이 가장 친숙한 부분이 바로 Heap Area일 것입니다. 그만큼 Java의 문제가 Memory 이슈에 집중이 되어 왔기 때문이라고 볼 수 있는데 이는 Java의 자동 Memory 해제, 즉 Garbage Collection과도 연관이 깊다고 볼 수 있습니다.


이러한 이유로 인해 많은 사람들이 Java의 메모리 구조는 곧 Heap이라는 오해를 하고 있다고 합니다. 그렇지만 Thread 공유의 정보는 Stack에 저장이 되고 Class나 Method 정보, Bytecode 등은 Method Area에 저장된다는 것을 앞서 설명한 바 있습니다. 이 Java Heap은 단지 Instance(Object)와 Array 객체 두 가지 종류만 저장되는 공간일 뿐입니다. 모든 Thread들에 의해 공유되는 영역입니다. 같은 애플리케이션을 사용하는 Thread 사이에서는 공유된 Heap Data를 이용할 때 동기화 이슈가 발생할 수 있습니다. 원래 각 애플리케이션은 철저히 분리되어 서로에게 영향을 줄 수 없지만 동일한 Instance를 공유하거나 Class Variable을 사용하는 경우, 모든 Thread들이 접근할 수 있기 때문에 동기화 문제가 수반됩니다.


JVM은 Java Heap에 Memory를 할당하는 Instruction(Bytecode로 new, newarray, anewarray, multianewarray)만 존재하고 메모리 해제를 위한 어떤 Java Code나 Bytecode도 존재하지 않습니다. Java Heap의 메모리 해제는 오로지 Garbage Collection을 통해서만 수행됩니다. JVM 스펙(Specification)은 이러한 원칙을 강하게 제시하고 있고 그 구현을 담당한 JVM 벤더들은 최대한 이를 따르고 있습니다. 그러나 이것은 원칙일 뿐 어떻게 구현하는지에 대해서는 전적으로 벤더들에게 일임하고 있습니다.


Garbage Collection 뿐만 아니라 Heap의 전반적인 구성도 특별히 정의된 바 없이 JVM을 구현하는 벤더에게 전권을 위임하고 있습니다. 게다가 Heap은 단순히 Array와 Object를 저장하는 공간에 지나지 않습니다. 그렇기 때문에 Heap의 동작 원리나 메커니즘에 대한 설명보다는 우리가 실제로 사용하는 Hotspot JVM이나 IBM JVM의 Heap 구조를 통해 전반적인 JVM Heap에 대한 이해를 돕는 것이 의미 있을 것이라 생각합니다.


Hotspot JVM은 미국의 Longview Technologies LLC라는 회사에서 1999년에 처음 발표된 JVM입니다. 이후 이 회사는 같은 해 SUN에 의해 인수되어 1.3버전부터 SUN의 기본적인 JVM이 되었습니다. Hotspot JVM은 현재 가장 일반적인 JVM 중의 하나로 Windows, Linux, Solaris는 물론 Mac OS와 기타 UJnix OS에도 탑재가 가능합니다. 특히 HP Unix에서도 Hotspot JVM을 제공하고 있습니다. 심지어 Oracle을 설치할 때 기본적으로 설치되어 구동되는 JVM 조차 Hotspot JVM입니다. 이는 제작사는 다르더라도 Hotspot JVM이라는 공통점을 가지고 있다는 의미입니다. 현재 국내에서 많이 사용되는 WAS가 Tmax사의 JEUS, Oracle사의 WebLogic, IBM사의 WebSphere라고 볼때 WebSphere를 제외하고는 모두 Hotspot JVM을 사용한다고 볼 수 있습니다. 물론 IBM AIX에서는 IBM JVM을 기본으로 제공하고 있습니다. 따라서 Sun, Oracle, HP, Windows, Linux, MacOS에서 제공하는 JVM은 Hotspot JVM으로 명명하고 IBM에서 제공하는 JVM은 IBM JVM이라 부르기도 합니다.



1) Hotspot JVM의 Heap 구조

Hotspot JVM의 Heap 구조에 대해 알아보겠습니다. Hotspot JVM은 익히 알려져 있듯이 크게 Young Generation과 Old Generation으로 나누어져 있습니다.


Young Generation은 Eden 영역과 Survivor 영역으로 구성되는데 Eden 영역은 Object가 Heap에 최초로 할당되는 장소이며 Eden 영역이 꽉 차게 되면 Object의 참조 여부를 따져 만약 참조가 되어 있는 Live Object이면 Survivor 영역으로 넘기고, 참조가 끊어진 Garbage Object이면 그냥 남겨 놓습니다. 모든 Live Object가 Survivor 영역으로 넘어가면 Eden 영역을 모두 청소(Scavenge)합니다.


Survivor 영역은 말 그대로 Eden 영역에서 살아남은 Object들이 잠시 머무르는 곳입니다. 이 Survivor 영역은 두 개로 구성되는데(Survivor1, Survivor2) Live Object를 대피시킬 때는 하나의 Survivor 영역만 사용하게 됩니다. 이러한 전반의 과정을 Minor GC라고 합니다. 


Young Generation에서 Live Object로 오래 살아남아 성숙된 Object는 Old Generation으로 이동하게 됩니다. 여기서 성숙된 Object란 의미는 애플리케이션에서 특정 회수 이상 참조되어 기준 Age를 초과한 Object를 말합니다. Old Generation 영역은 새로 Heap에 할당되는 Object가 들어오는 것이 아니라, 비교적 오랫동안 참조가 되어 이용되고 앞으로도 계속 사용될 확률이 높은 Object들을 저장하는 영역입니다. 이러한 Promotion 과정 중 Old Generation의 메모리도 충분하지 않으면 해당 영역에도 GC가 발생하는데 이를 가리켜 Full GC(Major GC)라고 합니다.


Perm 영역은 모통 Class의 Meta 정보나 Method의 Meta 정보, Static 변수와 상수 정보들이 저장되는 공간으로 흔히 메타데이터 저장 영역이라고도 합니다. 이 영역은 Java 8부터는 Native 영역으로 이동하여 Metaspace영역으로 변경되었습니다. (다만, 기존 Perm 영역에 존재하던 Static Object는 Hep 영역으로 옮겨져서 GC의 대상이 최대한 될 수 있도록 하였습니다)


Perm 영역 : 보통 Class의 Meta 정보나 Method의 Meta 정보, Static 변수와 상수 정보들이 저장되는 공간으로 메타데이터 저장 영역이라고 불리움.


현재 가장 많이 사용중인 Java 7과 Java 8의 구조적인 측면에서 변경 사항을 중심으로 좀 더 비교 설명하고자 합니다.


최근 Java 8에서 JVM 메모리 구조적인 개선 사항으로 Perm 영역이 Metaspace 영역으로 전환되고 기존 Perm 영역은 사라지게 되었습니다. Metaspace 영역은 Heap이 아닌 Native 메모리 영역으로 취급하게 됩니다. (Heap 영역은 JVM에 의해 관리된 영역이며, Native 메모리는 OS 레벨에서 관리하는 영역으로 구분) Metaspace가 Native 메모리를 이용함으로서 개발자는 영역 확보의 상한을 크게 의식할 필요가 없어지게 되었습니다.


 구분

상세 구분 

~ Java 7까지 버전(Perm) 

~ Java 8 버전(Metaspace) 

저장 정보 

 Class의 Meta 정보

저장 

저장 

Method의 Meta 정보 

저장 

저장 

Static Object 변수, 상수 

저장 

Heap 영역으로 이동 

 관리 포인트

메모리 관리(튜닝) 

Heap 영역 튜닝외에

Perm 영역 별도 튜닝 

Heap 영역 튜닝

Native 영역 동적 조정

(별도 옵션으로 조절 가능) 

GC 측면 

 GC 수행 대상

 Full GC 수행 대상

 Full GC 수행 대상

메모리 측면 

 메모리 크기(옵션)

 -XX: PermSize

-XX: MaxPermSize

 -XX: MetaspaceSize

-XX: MaxMetaspaceSize


Perm 영역과 Metaspace 영역은 해당 영역의 기본값이 시스템별로 크게 다를 수 있으므로 해당 영역에 대한 튜닝 시 시스템별 초기치와 최대치를 확인하여야 합니다. 개인 용도로 많이 사용하는 VMWare의 리눅스 환경을 예를 들어 해당 수치를 확인해보겠습니다.


Case 1. Java 7의 Perm 초기치와 최대치 확인

root@:/user/jdk/jdk1.7.0_40_x64/bin> ./Java -XX:+PrintFlagsFinal -version -server | grep "PermSize"

uintx AdaptivePermSizeWeight     = 20                 {product}

uintx MaxPermSize                        = 85983232     {pd product}

uintx PermSize                               = 21757952      {pd product}

Java version "1.7.0_40"

Java(TM) SE Runtime Environment (build 1.7.0_40-b43)

Java HotSpot(TM) 64-Bit Server VM (build 24.0-b56, mixed mode)


Case 2. Java 8의 Metaspace 초기치와 최대치 확인

root@:/usr/jdk/jdk1.8.0_101_x64/bin> ./Java -XX:+PrintFlagsFinal -version -server | grep "MetaspaceSize"

uint InitialBootClassLoaderMetaspaceSize         = 4194304 {product}

uintx MaxMetaspaceSize                                    = 18446744073709547520 {product}

uintx MetaspaceSize                                           = 21807104 {pd product}

Java version "1.8.0_101"

Java(TM) SE Runtime Environment (build 1.8.0_101-b13)

Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

Java HotSpot(TM) 64-Bit Server VM (build 24.0-b56, mixed mode)


위와 같이 측정 결과치에서 보듯이 Java 7의 MaxPermSize는 85,983,232 Byte(약 82MB)인 반면, Java 8의 MaxMetaspaceSize는 18,446,744,073,709,547,520 Byte(약 16Exabyte)라는 매우 큰 수치입니다. 여기서 16ExaByte는 64bit 프로세서가 취급할 수 있는 메모리 상한치라 할 수 있습니다.