JAVA/JAVA | Spring 학습기록

[Java] PermGen 영역 대신 Metaspace가 도입된 Java 8 이후의 JVM 구조 및 JVM 튜닝 맛보기

kth990303 2023. 1. 27. 02:30
반응형

jdk 1.7 이하, 즉 Java 8 이전 버전에서의 JVM 메모리 구조(Runtime Data Area)Java 8 이후의 JVM 구조는 아래와 같은 차이가 존재한다.

출처: https://www.programmersought.com/article/4905216600/

Eden 영역과 Survivor, Old 영역의 존재는 변함이 없다. 하지만 Java 8 이후부터는 Permanent Heap(이하 Permgen) 영역이 제거되고 Metaspace 영역이 생성된 것이다. 특히 눈여겨볼만한 점은 Permgen은 Heap 영역에 속했었는데 Metaspace는 Native Memory 즉 OS 관리대상에 속한다는 것이다.


근데 Permgen(Java 7 이하), Metaspace(Java 8 이후)가 뭐하는 녀석인데?

여기서 잠깐, Permgen (Metaspace)에서는 무엇을 관리하는걸까?

자, 잠깐 Java 실행 원리를 확인해보는 시간을 가져보자.

Java 실행 원리 - 출처: https://www.geeksforgeeks.org/jvm-works-jvm-architecture/

우리가 열심히 공부해왔던 Java 실행 원리이다.

java 코드를 작성하면 .class 파일로 컴파일되고, 이 파일이 Class Loader에 의해 JVM 메모리에 로드가 된다.

JVM 메모리에는 그림에서 볼 수 있다시피 Method Area, Heap 등이 있는데, 여기서 Permgen(Java 7 이하) 및 Metaspace(Java 8 이후)는 저 Method Area에 해당하는 부분이다.

 

method area에선 Klass(java 클래스 메타정보 참조) 구조, constant pool, annotations를 관리한다. 클래스 데이터들을 관리하는 곳이기 때문에, 클래스가 많아질수록 method area 메모리 사용량도 증가하는 셈이다.

또, String Constant pool 역시 Constant pool이므로 문자열도 여기서 관리된다고 보면 된다.

 

그리고 이 method area는 모든 스레드들이 공유한다. 인스턴스 생성에 필요한 정보들을 관리하기 때문이다.


실제 배포되는 서비스에서 Permgen, Metaspace 정보 확인해보기

실제로 내편 서비스(https://github.com/woowacourse-teams/2022-nae-pyeon)의 인스턴스에 접속해서 힙 사이즈와 permsize를 검색해보았다. (참고로 내편 서비스는 Java 11 환경이며, RAM은 4GB이다.)

java -XX:+PrintFlagsFinal -version | grep -iE 'heapsize|permsize'

maxHeapSize가 약 1024MB에 근접한 972MB임을 확인

RAM이 4GB이므로 

initialHeapSize가 약 0.062GB (4GB / 64),

maxHeapSize가 약 1GB (4GB / 4)

로 설정된 것을 확인할 수 있다.

 

하지만, permSize는 나오지 않음을 확인할 수 있는데 이는 Java 8 이후는 Metaspace를 쓰기 때문이다.

Metaspace 사이즈 정보를 아래 명령어로 검색해보았다.

java -XX:+PrintFlagsFinal -version -server | grep MetaspaceSize

metaspace 정보가 나오며, 64bit 프로세서 최고 메모리 상한으로 maxMetaspaceSize가 설정된 것을 확인 가능

Metaspace 크기는 아까 본 Heap 사이즈에 비해 훨씬 큼을 확인할 수 있다.

Metaspace는 Native Memory 자원을 활용하며, 필요에 따라 자동적으로 증가한다. 그렇기 때문에 MaxMetaspaceSize 값을 변경할 일은 별로 없다고 하나 `-XX:MaxMetaspaceSize` 명령어로 변경이 가능하긴 하다고 한다.


Permgen을 제거하고 Metaspace로 바뀐 이유?

Permgen은 위에서도 말했듯이 heap 영역에 속한다. 그리고 이 Permgen은 기본적으로 메모리량이 적어 OOM (OutOfMemoryError)를 발생시킬 확률이 높다. 이게 사실상 근본적인 이유다.

 

The biggest disadvantage of PermGen is that it contains a limited size which leads to an OutOfMemoryError. The default size of PermGen memory is 64 MB on 32-bit JVM and 82 MB on the 64-bit version. Due to this, JVM had to change the size of this memory by frequently performing Garbage collection which is a costly operation.

출처: https://www.geeksforgeeks.org/metaspace-in-java-8-with-examples/

 

Method area 관리 대상 정보들을 OS가 관리하도록 하여 사이즈 제한에 조금이나마 자유로워질 수 있도록 바뀐 것이라 보면 된다.

 

또한, Permgen은 고정된 크기로 설정돼있어 Full GC 작업이 많이 일어났지만 Metaspace는 필요에 따라 사이즈가 변동되기 때문에 보다 효율적으로 GC 작업을 할 수 있다고 한다. Full GC 작업은 Stop-the-world 과정으로 애플리케이션이 잠시 중단될 수 있기 때문에 최소화할수록 좋은데 Metaspace의 성질 덕분에 이러한 부분에서 이점을 챙기게 된 것.


JVM 튜닝 맛보기

위에서 잠깐 Stop-the-world로 인한 Full GC 얘기를 했는데, 이 과정을 최소화하면서 오버헤드가 발생하지 않도록 JVM 튜닝을 하게 된다. JVM 튜닝을 하기 위해선 어떻게 해야 할까?

 

JVM 튜닝은 깊게 들어갈수록 굉장히 어려워지기 때문에 여기서는 맛만 볼 것이다.

위에서 봤듯이 Java Heap 영역에는 Eden, Survivor 0 (이하 s0), Survivor 1 (이하 s1), Old 영역이 있다.

Eden, s0, s1 영역을 합쳐서 Young 영역이라 하는데, Young 영역에서 특정 기간동안 계속해서 쓰이고 있는 살아있는 객체들은 Old 영역으로 이동하게 된다.

 

Young 영역 객체들을 관리하고 지워주는 작업을 Minor GC, Old 영역 객체들을 관리하고 지워주는 작업을 Major GC 라고 한다.

Minor GC 작업은 Eden 영역이 객체들로 할당이 꽉 차서 더 이상 저장할 수 없을 경우 발생하게 되며, 이 때 Eden -> s0 또는 s1으로 이동하게 된다. Minor GC 작업이 일어날 때마다 Eden && s0 (또는 s1) -> s1 (또는 s0)으로 이동하게 되는데, 이 때 s0과 s1에 있는 모든 객체가 이동한다. 즉, s0과 s1 둘 중 하나의 영역만 사용되며 동시에 사용되는 경우는 존재하지 않는다. 메모리 파편화 방지를 위함이다.

 

그리고 이 Young 영역에서 살아남는 객체들이 Old 영역으로 할당되게 된다. 

Old 영역이 꽉 차서 더 이상 저장할 수 없을 때 Major GC 과정이 발생하는데 이 때 애플리케이션이 잠깐 중단하는 Stop-the-world 현상이 발생한다!

 

 

여기서부터는 이론적으로만 학습한 부분입니다.

실제로 튜닝 경험이 없으니 참고용 정도로만 봐주세요

 

 

JVM 튜닝 맛보기인데 왜 갑자기 Heap에 대해 공부하는 딴소리를 했냐면 결국 힙사이즈로 인해서 GC 작업이 처리되는 것이기 때문이다.

힙 사이즈는 -Xms(초기 힙 사이즈 설정), -Xmx(최대 힙 사이즈 설정)으로 설정할 수 있다. 

보통 JVM 튜닝을 진행하면 -Xms, -Xmx 옵션을 똑같이 하는 것을 권고한다고 한다. -Xms, -Xmx 설정이 다르면 메모리를 늘려달라고 요청하는 부하가 발생하기 때문이라고 한다.

 

그리고 minor GC 작업 대상인 New 영역의 크기가 너무 작으면 Old 영역으로 넘어가는 객체가 많아져 Stop-the-world가 자주 발생할 수 있다고 한다. 따라서 New 영역의 크기를 적절히 세팅해주어야 한다. 그런데 또 oracle에서 G1 GC(java 11 default GC)를 사용할 경우 New Generation 영역 크기를 웬만해선 손대지 말라고 안내하고 있다.

Recommendations
When you evaluate and fine-tune G1 GC, keep the following recommendations in mind:

Young Generation Size: Avoid explicitly setting young generation size with the -Xmn option or any or other related option such as -XX:NewRatio. Fixing the size of the young generation overrides the target pause-time goal.

출처: https://www.oracle.com/technical-resources/articles/java/g1gc.html

 

Java 8 기본 GC였던 parallel GC에선 많은 사용자들이 New Generation 사이즈를 힙 사이즈에 따라 적절히 변경해주면서 튜닝을 하는 글을 흔히 볼 수 있었다. 하지만 G1 GC는 또 상황이 다른가보다.

 

이렇듯, JVM 및 GC 튜닝은 사용하고 있는 힙 사이즈와 GC의 종류, JVM에서 차지하고 있는 메모리에 따라 상황이 달라지기 때문에 충분한 모니터링을 통해 적재적소로 하는 것이 중요한 듯하다.

 

 

메모리 누수 확인하는 방법

Old Generation 크기가 지속적으로 증가할 경우, 메모리 누수를 의심해볼 수 있다.

어디에서 메모리 누수가 발생하는지는 dump 파일을 MAT 툴로 분석해보면 확인해볼 수 있다.

dump 파일을 만들어서 본격적으로 분석해보고 싶다면 아래 명령어를 입력하면 된다.

jmap -dump:live,format=b,file=heap.bin {PID}

dump 파일 생성

 

dump 파일 분석은 이후에 차차 공부해봐야 될 듯하다.


참고자료

반응형