JDK로 궁금증을 해결해보자!

L-cloud·2023년 7월 26일
0

스터디

목록 보기
4/5
post-thumbnail

JVM에 관해 설명하기에는 상당히 부족한 글입니다. 주된 주제는 JVM 옵션을 사용해서 “어떻게 궁금증을 해결해 나가는가?”입니다

본 글에서 알아볼 내용
1. 검색하면 나오는 JVM에 대한 이야기
2. 잘 모르는 것을 JDK로 확인하기
3. static 영역 메모리를 heap dump와 -verbose:class를 통해 확인해 보는 예제

Java의 특징

Java는 하이브리드 언어라 불립니다. 우선 Java는 .java 파일을 javac 명령어를 통해 .class 파일로 컴파일합니다. 이 .class 파일은 자신의 환경(OS 종속적)에 맞는 JVM 위에서 돌아갈 수 있는 Byte code입니다. 이를 통해 WORA (Write Once Run Anywhere)를 지킬 수 있습니다. 그림으로 보면 아래 사진 같겠죠.

또한 Java는 C/C++와 다르게 프로그래머가 직접 메모리 할당, 해제를 하거나 native thread를 사용할 수 없고 JVM을 거쳐서 사용합니다. Java를 사용하면 포인터에서 해방이 아닌…새로운 개념을 배워야 하는 굴레에 빠지는 것이죠. 결국 Java를 더욱 잘 이해하기 위해 JVM에 대해 잘 알아야 합니다. 이를 위해 JVM에 대해 검색해 보면 대략 아래와 같은 이미지와 그에 대한 설명이 나옵니다.

JVM

Class Loader

.class 파일을 메모리에 로드합니다. 클래스가 여러 개라면 Main() 매서드를 포함하는 클래스를 우선 로드합니다. 로드, 링킹, 초기화 단계를 거칩니다.

Method Area

Runtime Constant Pool, class와 interface 정보들이 저장됩니다. static 변수도 저장 됩니다.

Method Area 와 Heap은 모든 스레드가 공유합니다. 이외의 영역은 스레드 별로 각자 가지고 있습니다.

stack

stack frame의 형태로 저장됩니다. 로컬 변수가 저장되는 곳입니다. primative 타입이면 값 자체도 저장되며, 객체의 경우 Heap 메모리에 있는 reference를 가지고 있습니다. 객체가 저장되는 모습은 파이썬과 비슷하며 여기의 그림을 참고해 보세요
stack frame에는 아래 세 가지가 저장됩니다

  • Local Variables
  • Operand Stack
  • Constant Pool Reference

PC register

물리 PC(program counter)와 비슷하다. 현재 쓰레드에서 어디까지 실행했는지 정보 등을 가지고 있습니다.

Native method stack

Java 말고 c/c++같은 native 함수들 stack입니다.


위 설명을 그림으로 다시 표현하면 아래 그림과 같겠죠.

위 그림은 Java 7 기준이며 8부터는 Permanet는 Meta space로 변경되었습니다.
즉 JDK 7은 아래 사진과 같았고

JDK 8 이상 부터는 아래 사진과 같습니다.

왜 이러한 변화가 있었을까요?

Permanet 영역은 JVM에 의해 관리되는 영역이었습니다. 도커에서 Java 7 버전을 설치한 뒤 Perm Size를 설정하지 않았을 때의 결과를 봅시다. 비트 단위이기 때문에 1024를 두 번 나눠주면 되고 대략 166MB와 20MB가 됩니다. 만약 MaxPermSize를 초과하면 어떻게 될까요? 네 OOME와 함께 JVM이 다운됩니다. 추가로 메모리 확보가 어렵기에 GC도 자주 일어나고 최악의 경우 다운이 되어버립니다.

root@c9037c512a44:/ java -XX:+PrintFlagsFinal -version -server | grep PermSize
    uintx AdaptivePermSizeWeight                    = 20              {product}
    uintx MaxPermSize                               = 174063616       {pd product}
    uintx PermSize                                  = 21757952        {pd product}
java version "1.7.0_221"
OpenJDK Runtime Environment (IcedTea 2.6.18) (7u221-2.6.18-1~deb8u1)
OpenJDK 64-Bit Server VM (build 24.221-b02, mixed mode)

하지만 Metaspace는 OS 레벨에서 관리되는 Native 영역입니다. 도커에서 Java 8 버전을 설치한 뒤 MetaspaceSize를 확인하면 시스템이 허용하는 최대치로 메모리를 할당할 수 있는 것을 볼 수 있습니다.

root@0847f4afce69:/ java -XX:+PrintFlagsFinal -version -server | grep MetaspaceSize
    uintx InitialBootClassLoaderMetaspaceSize       = 4194304                             {product}
    uintx MaxMetaspaceSize                          = 18446744073709547520                    {product}
    uintx MetaspaceSize                             = 21807104                            {pd product}
openjdk version "1.8.0_342"
OpenJDK Runtime Environment (build 1.8.0_342-b07)
OpenJDK 64-Bit Server VM (build 25.342-b07, mixed mode)

즉 초기 대략 21MB가 있고 `Metaspace`가 부족해지면 메모리를 OS에게 직접 할당받을 수 있습니다. OOME로 부터 비교적 자유로워졌습니다. 물론 메모리 관리를 제대로 하지 않으면 이전과 다르게 JVM뿐 아니라 OS에 올라온 모든 프로세스가 위험할 수 있겠죠?

이외에도 static 변수가 permanent generation에서 Heap으로 이동해 가기도 했습니다.

여기까지 제가 이해한 내용이며 인터넷에서 쉽게 찾을 수 있는 내용입니다. 거의 복사해 온 글이기에 아래 있는 참고한 링크를 직접 읽어보시는 것이 훨씬 도움이 됩니다!


그래서 static이 어떻게 메모리를 낭비하고 어디에 저장되는데?

Python 메모리 영역을 공부해 보았기에 비슷한 부분이 꽤 있어 이해에 도움이 되었습니다. 특히 객체가 저장되는 영역은 정말 비슷하더라고요. 하지만 static은 이해하는데 조금 어려움을 겪었습니다.

제가 겪은 어려움은 2개입니다.

  1. static은 언제 메모리를 차지하는가? static allocation을 의미하는가?
  2. static은 어디에 저장되는가? Java 8부터 heap이라며.. 그런데 Method Area에 저장된다는 말은 또 뭐야!

많은 글에서 static은 메모리를 미리 차지하기에 메모리 낭비가 될 수 있다! 라고 말을 많이 하는데 그럼 static은 Java가 실행될 때 모두 메모리 영역을 차지하고 종료될 때까지 사라지지 않는 것일까요? 또 static은heapMethod Area 둘 중 어디에 저장되는 것일까요? 글마다 약간씩 단어 표현이 다르기도 하고 무언가 명쾌한 글을 찾지 못해서 그냥 직접 실험을 통해 알아보고자 합니다!

JDK를 잘 활용해 보자!

JDK에는 생각보다 많은 도구가 있습니다. 설치된 JDK를 보시면 jmap, javap, javac, jps 등을 보실 수 있습니다. 본 글은 이를 설명해 주는 글이 아니니 JDK를 살펴보시면서 자신에게 필요한 것을 사용하시면 될듯합니다! 저는 jcmd를 통해 간단히 heap 영역을 살펴보았습니다.

우선 사용할 자바 코드를 소개합니다. class1.java이고

// Class1
public class Class1 {
	static int [] s = new int [1000000];
	static String test = "Super test";
	double k = 1000000000.515151321651; 
}

class2.java입니다.

import java.lang.Thread;

public class Class2 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Before load class1");
        System.out.println("---------------------------");
        Thread.sleep(10000); // heap을 확인하기 위한 sleep
        System.out.println("After load class1");
        System.out.println(Class1.test);
        Thread.sleep(10000);
    }
}

class1은 class2에서 호출이 됩니다. class2에서 class1을 호출하고 있으니 class2를 실행하면 class1에 있는 static 배열을 즉시 메모리에 올라올까요? 간단히 확인해 봅시다.

우선 컴파일해 준 뒤 Class2를 실행시켜 줍니다.

java Class2
Before load class1
---------------------------

그다음 Class1.test가 호출되기 전에 얼른 heap 정보를 확인해 봅시다.

jcmd 37936 GC.heap_info
37936:
garbage-first heap   total 262144K, used 1024K [0x0000000700000000, 0x0000000800000000)
region size 1024K, 2 young (2048K), 0 survivors (0K)
Metaspace       used 3411K, capacity 4486K, committed 4864K, reserved 1056768K
class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

그 다음 Class1.test가 호출된 이후를 확인해 보았습니다.

After load class1
Super test 

-------------------- 
//호출된 이후 jcmd확인

jcmd 37936 GC.heap_info
garbage-first heap   total 262144K, used 5120K [0x0000000700000000, 0x0000000800000000)
region size 1024K, 2 young (2048K), 0 survivors (0K)
Metaspace       used 3414K, capacity 4486K, committed 4864K, reserved 1056768K
class space    used 304K, capacity 386K, committed 512K, reserved 1048576K

heap의 메모리 사용량과 Metaspace의 메모리 사용량이 올라간 것을 확인할 수 있습니다! 정확히는 모르겠지만 heap 영역에 static 배열이 저장되는 것 같지 않나요? 더 자세히 보기 위해 heap 영역의 snapshot을 찍어서 확인해 봅시다!

아래는 heap영역의 스냅샷입니다. heap 영역에 Class1의 Static Data가 들어가 있음을 확인할 수 있습니다!


heap에 올라가는 타이밍을 더 정확하게 보기 위해서는 After load class1 을 출력 전후로 snapshot을 찍어서 확인해 보면 되겠죠? (신기하게도 primivite type인 double도 heap에 올라와 있습니다. 추측이지만 Method Area에 heap을 가르키는 레퍼런스가 있는 것이 아닐까 합니다. 이렇게 되면 어떻게 서로 다른 스레드에서 동일한 static에 접근할 수 있는지 추측이 되죠?)

여기서 저는 문득 Class Loader가 떠올랐습니다. 그럼 Class Loader가 동적으로 필요할 때 클래스를 loading 하는 것인가? 하는 의문이 들었습니다. 그래서 -verbose:class를 통해 직접 눈으로 확인해 보았습니다.

java -verbose:class Class2
[0.010s][info][class,load] opened: /Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home/lib/modules
....
[0.100s][info][class,load] java.nio.charset.CoderResult source: jrt:/java.base
Before load class1
---------------------------
After load class1
[15.104s][info][class,load] Class1 source: file:/Users/jongrokbaek/Desktop/docker/
Super test
[30.107s][info][class,load] jdk.internal.misc.TerminatingThreadLocal$1 source: jrt:/java.base
[30.107s][info][class,load] java.lang.Shutdown source: jrt:/java.base
[30.107s][info][class,load] java.lang.Shutdown$Lock source: jrt:/java.base

정말 클래스가 호출될 때 load 되는 것을 확인할 수 있습니다! 진작에 정확한 개념을 알고 있었더라면 혼란과 삽질을 안 해도 되었을 텐데 참 돌아 돌아왔습니다. 결론은 Java는 동적 연결하기에 클래스가 호출되는 시점이 static이 메모리에 쌓이는 시점이다!

그럼 load 된 클래스와 static 변수들은 언제 할당이 해제되는 것일까요? 바로 클래스가 언로드 될 때입니다. 이 경우도 JDK를 가지고 실험해 볼 수 있지 않을까요? 사실 실험을 해보려 하였으나 글도 길어지고 긴급히 할 일이 생겨서 다음 글에서 추가하도록 하겠습니다!

긴 글 읽어주셔서 감사합니다.

출처

Java compile
JVM1
JVM2
Java Permanet1
Java8 Permanet2
Java 11 JDK
Dynamic loading

profile
내가 배운 것 정리

2개의 댓글

comment-user-thumbnail
2023년 7월 26일

많은 도움이 되었습니다, 감사합니다.

1개의 답글