JAVA

JVM GC 후 Heap 에게 할당된 물리 메모리 OS에게 회수 여부

소농배 2022. 1. 3. 18:02

https://woooongs.tistory.com/85

 

-Xms 보다 Memory 사용량이 더 적은 이유

Logstash 는 JVM 에서 동작중이며 -Xms (최소 힙 크기 사이즈) 는 1g 로 실행되었다. 하지만 메트릭에서 보는 것 처럼 Java process 가 사용하는 메모리는 572MB 로 최소 힙 크기보다 적은 양의 메모리를 사

woooongs.tistory.com

 

위 글에서 최소 힙 사이즈보다 사용중인 물리 메모리가 더 작은 이유에 대해서 확인해보았다.

JVM 은 가상메모리에는 최소 힙 사이즈 만큼 공간을 할당하지만 실제로 객체를 힙에 저장하기 전까지는 물리 메모리를 점유하지 않는다.

 

하지만 위 글에서 해결되지 않는 의문이 있었다. 

객체가 실제로 힙에 저장될때 Java process RSS 사용량이 늘어나지만 GC 가 발생했을때는 RSS 가 줄어들지 않는 것이다.

 

어떤 이유에서인지 정확하게 알지 못하지만 JVM 에게 할당된 물리 메모리는 GC 가 발생한다고 하더라도 OS 에게 즉시 반환되지 않는다고 한다.

이 내용을 확인하고자 테스트를 진행하였다.


테스트 코드 및 실행 커맨드

import java.util.ArrayList;
import java.util.Arrays;

public class Main {
	public static void main(String[] args) throws InterruptedException {

		for (int j = 0 ; j < 100; j++) {
			ArrayList<char[]> arrayList = new ArrayList<>();
			        for (int i = 0 ; i < 10; i++) {
				Thread.sleep(1000);
				System.out.println("Count : " + i);
				char[] chars = new char[1000000];
				Arrays.fill(chars, (char) i);
				arrayList.add(chars);
			}
			Thread.sleep(5000);
			System.out.println("GC Performed");
		}
	}
}

 

java -Xms30m -Xmx30m -verbose:gc -XX:+UseG1GC Main

 

-Xms 와 -Xmx 값은 30M 으로 Heap 사이즈는 30MB 고정.

반복문을 돌면서 약 1M char 배열을 10번 생성후 GC 가 일어나도록 참조를 잃는다.


테스트 결과

무제.mov
16.82MB

터미널1) jstat 명령어로 GC 로 인해 각 영역의 메모리 사용량이 줄어드는것을 확인

터미널2) top 명령어로 자바 프로세스의 물리 메모리 점유량 (RES) 를 확인

터미널3) GC 로그 모니터링


00:00) 자바 프로세스를 실행하자마자 처음 찍힌 top 명령어의 RES 는 26MB 이다.

-Xms, -Xmx 모두 30MB 이지만 Java Process 가 사용하는 물리 메모리는 26MB 로 Heap 사이즈만큼 물리 메모리를 점유하지 않는것을 확인할 수 있다.

 

00:23) 객체를 할당함에 따라 RES 가 계속 증가하는것을 확인할 수 있다. 이를 통해 힙에 객체가 할당될때 JVM 이 물리 메모리를 점유하고 RES 가 증가하는것을 확인할 수 있다. 약 52MB 까지 사용량이 증가하였다.

Old Generation 에 객체들이 계속 쌓이고 있음을 확인할 수 있다.

 

00:37) 첫번째 Full GC 발생. GC 로그에서 알 수 있지만 Heap 영역 사용량이 25MB -> 5.9MB 로 줄어들었다.

GC 이전 - Old영역 사용량 : 24MB

GC 이후 - Old영역 사용량 : 8.8MB

GC 이후에 분명히 힙 영역의 공간이 확보되었다. 하지만 Java process 의 RES 를 보면

 

01:06) Full GC 가 한번 더 발생하였다. GC 에 의해서 분명히 Heap 영역의 공간이 확보된 것을 확인할 수 있다.

하지만 마찬가지로 JAVA process 의 RES 는 줄지 않았다. 또한 시간이 경과됨에 따라 꾸준히 증가하던 RES 는 거의 증가하지 않는 상태가 되었다.


결론

이 테스트로 확인해보고 싶었던 것은 JVM 이 힙에 객체를 할당할때 물리 메모리 사용량이 증가하지만 해당 객체가 GC 가 되었을때 JVM 이미 점유한 물리 메모리는 OS 에게 반환되지 않는다는 것을 확인하고 싶었다. 실제로 GC 가 발생하여도 물리 메모리 사용량은 줄어들지 않았다. 그렇다는 것은 JVM 은 힙 영역을 사용하기 위해 한번 할당 받은 물리 메모리에 객체를 다시 할당하여 쓰고 있는것으로 보인다. 

 

왜 JVM 이 한번 할당 받은 메모리를 OS 에게 반환하지 않는지는 더 확인을 해보아야 겠다. 테스트를 진행한 Linux 는 Swap 이 되지 않도록 설정되어있어서 swap out 되지 않기 때문일수도 있고 JVM 이 Heap 에 접근을 자주하니 Working Set Model 에 의해서 자주 접근되는 Heap 이 할당된 Frame 은 제거 되지 않았을 수도 있을 것 같은데 조금 더 확인이 필요하다.


번외

Heap 은 30MB 뿐인데 물리 메모리 사용량은 50MB 이상 나오니 어떤 것이 메모리를 사용하고 있는지 확인하고싶었다. 

pmap -x 19408 | sort -k 3 -n -r | more
total kB         3583588   59124   41200
00000000fe200000   31232   30368   30368 rw---   [ anon ]
00007f16aee8d000   13760   11572       0 r-x-- libjvm.so
00007f16492fb000    4352    2516    2516 rw---   [ anon ]
00007f164912a000    1860    1860       0 r--s- rt.jar
00007f16b052e000    1948    1580       0 r-x-- libc-2.27.so
00007f16a8000000    1672    1412    1412 rw---   [ anon ]
00007f16ac019000    9068     880     880 rw---   [ anon ]
00007f161c000000    1044     852     852 rw---   [ anon ]
00007f169802d000    3868     796     796 rw---   [ anon ]
00007f16aeb04000    1508     744       0 r-x-- libstdc++.so.6.0.25

Java process 가 사용하는 메모리를 확인한 것인데 가장 상단에 있는 내역이 Heap 메모리이다.

VIRT = 30.5MB , RES = 29.6MB 로 설정한 대로 30MB 이내로 동작중이였으며 하위에 메모리에 올라가있는 libjvm.so 와 같은 라이브러리들이 메모리를 사용하고 있는 것을 볼 수 있다.