이번 포스팅으로 Immutable 객체의 상태(값)을 반복해서 변경하면 무슨 일이 발생하는지 알아보자
관찰점: 힙 메모리가 어떻게 변하는지, 힙 메모리에 올라온 객체를 재사용하는지, GC가 몇 번 발생하는지
Visual VM
Heap 메모리를 관찰하는 Tool로 VisualVM을 사용할 것이다
이 프로그램은 몇몇 플러그인을 설치할 수 있는데, 나는 GC를 보기 위해 Visual GC 플러그인을 설치했다
예제 코드
Immutable(Integer)
실행결과를 예상해보자
이전 포스팅 글들을 봤다면 Integer를 이런 식으로 더하다보면 새로운 힙 메모리가 할당된다고 예상할 수 있을 것이다
x += 10; // 이 코드는 아래 코드로 변환되어서 실행
x = Integer.valueOf(x.intValue() + 10); // 위의 코드가 내부적으로 이런 방식으로 실행 (= new Integer)
메모리 해시값을 계속 출력하는 건 I/O 과부하를 일으키니깐 함수에 들어간 객체의 해시주소값과 10회 내의 반복을 돌 때의 객체의 해시주소값을 출력해봤다
모두 다 다른 메모리 주소를 사용하는 것을 알 수 있다. 왜냐하면 불변이니깐
Visual VM의 모니터 탭을 살펴봤다
이 다양한 그래프 중 우리의 관심사는 메모리 영역 중 Heap이기때문에 여기부분만 보면 된다
흠.... 뭔가 JVM 힙메모리 최대가 4GB라서 잘 안나오는것 같기도해서 8GB로 늘려줘야겠다
-Xmx8G -Xms8G
그리고 왜 힙메모리 최소와 최대를 같게 설정했는가?에 대한 질문은 Chat GPT의 답변으로 대신해본다
아까보단 확실히 정형화된 그래프가 나오는 것 같다
GC를 보기위해 추가한 Visual GC플러그인의 결과를 확인하기 위해 Visual GC 탭으로 이동해봤다
하단부에 보이는 Histogram은 JVM 힙 메모리 내에서 객체 크기의 분포를 나타낸다(X축: 객체의 크기 - byte, Y축: 객체의 개수)
이 분포 그래프는 Eden, Survivor, Old 영역에 있는 객체들의 분포 그래프이다
- Eden: 새롭게 생성된 객체가 먼저 할당되는 영역
- Survivor: Eden 영역에서 살아남은 객체가 옮겨지는 영역
- Old (Tenured): 장기간 생존한 객체가 옮겨지는 영역
분석
Monitor 탭: 힙 메모리를 4GB씩 사용하다가 GC가 일어나서 잠시 내려갔다가 또 다시 오르는 모습을 확인할 수 있었다
Visual GC 탭: GC Time을 보면 GC가 25번 발생했다는 것을 알 수 있다
개선
불변객체를 사용하면서, Heap 메모리가 저렇게 치솟게 하지 않고 싶다면?
GC를 직접 해보자
Monitor 탭을 확인해보자
힙 메모리의 그래프가 하나도 치솟지 않은 모습을 볼 수 있다
다만.. 왼쪽 그래프를 보면 CPU를 52%나 사용하고 있으며 GC가 11% 발생하고 있다(이전 그래프는 CPU: 9.3%, GC: 0.0%)
아마 모니터링 탭의 GC활동 상태는 직접적으로 GC를 실행했을때만 집계되는 모양이다(아님 제보점..)
이번에는 Visual GC 탭을 살펴봤다
GC가 8045번이나 발생했다. 마지막 사유는 System.gc()때문이란다
(GC가 25번 발생했던 이전 그래프에서는 G1 Evacuation Pause때문에 GC가 작동했었다는 사실)
그럼 이렇게 개선된걸까?
NoNo..
GC가 발생하면 STW(Stop-The-World)이벤트가 발생할 수 있다. 그리고 메모리뿐만 아니라 CPU의 사용량 또한 항상 모니터링되어야 할 중요 지표이다
- 자주 발생하는 GC가 좋은 경우: 메모리 누수를 방지하고, 메모리를 자주 회수함. 이것으로 인해 STW 이벤트가 짧게 유지되는 것이 중요한 실시간/인터랙티브 애플리케이션에 유리
- 자주 발생하는 GC가 나쁜 경우: CPU 리소스를 많이 사용하거나, 긴 STW 이벤트로 인해 성능 저하가 발생할 수 있는 상황에서는 GC가 불리
그럼 Heap메모리의 누수를 방지하며 직접 GC를 발생시키지 않고, 값을 업데이트 하는 방법은 없을까?
Mutable(MutableInteger)
가변객체이기때문에 같은 해시 메모리 주소를 사용할 것으로 예상된다
그럼 힙에 계속 객체를 올리지 않게 되기 때문에 힙 메모리가 치솟지 않을 것이라고 예상할 수 있을 것이다
VisualVM으로 확인해보자
2분이나 돌렸는데도 CPU 사용량은 12%, GC 활동은 0.0%, Heap 메모리 사용량도 1GB 근처에도 안 간 것을 볼 수 있다
그럼 Visual GC 탭을 살펴보자
GC Time을 보면 GC가 한번도 발생하지 않은 것을 볼 수 있다!
오~!!! 그럼 불변객체보다 가변객체로 코딩하면 모든 문제를 해결 할 수 있겠네~!!
라는 결론을 도출하면 안.된.다
가변객체는 순수 함수에서 사용할 때 어떤 사이드이펙트가 발생할지 예상 할 수 없고, 멀티스레드에서 안전하지 않은 경우가 많다
요약
- GC에 대한 이해가 있고, STW가 어플리케이션에 미치는 영향을 알고 있고, GC를 다뤄본 경험이 있다면 GC를 사용해봐도 되지 않을까?
- DB에서 읽어온 데이터를 처리하거나(Transaction) 하는게 아닌, 단순 값만을 업데이트하는 아주 간단한 프로젝트 또는 main 메서드에서만 작동하는 간단한 작업의 경우라면 가변객체를 사용하는 것도 나쁘지 않을 수 있다
- 가변객체이면서 멀티스레드에서 처리해야하는 경우는 AtomicXXX 클래스를 활용해보자. 원자적 연산을 제공하기 때문이다
Extra
추가로 간단하게 문자열을 다루는 경우도 확인해봤다
Immutable(String)
Monitor 탭
Integer와는 비슷하지만 더 심각하군..
이번에는 Visual GC 탭을 살펴보자
난리난 모습을 볼 수 있다ㅋㅋㅋ케
이것을 해결하기 위해 StringBuilder 또는 StringBuffer를 선택할 수 있다
Mutable(StringBuilder)
과연 이 코드를 실행하면 안전할까..? 실행하기전에 미리 생각해보자
실행시키면 프로세스가 조금 돌다가 OOM 에러를 만나며 종료가 된다
이유는?
문자열을 최종적으로 append하기 전에 ensureCapacityInternal 메서드가 호출된다
내부적으로는 Arrays.copyOf()가 호출된다
더 깊이 들어가보면 System.arraycopy 메서드가 호출된다
결국, JVM 내에서 호출된다는 어노테이션인 @IntrinsicCandidate과 native 키워드를 확인 할 수 있다
이제 배열을 복사하는 과정에서 에러가 발생했다는 것을 알 수 있다
개선
이 문제를 코드로 방지하면서 개선해보자
어떻게 할 수 있을까?
100000 * n번째마다 StringBuilder의 길이를 0으로 설정해서 기존 배열을 복사할 때 OOM을 방지할 수 있다
* setLength(0):하지만 실제로 내부 배열의 크기는 변경되지 않는다(배열의 용량이 줄어드는 것이 아니라, 단지 다음 문자열이 추가될 때 배열의 처음부터 다시 쓰이게 되는 것)
2분이 지났는데도 OOM이 발생하지 않는 모습을 확인할 수 있다
Monitor 탭을 확인해보자
평온한 힙메모리 상태를 확인할 수 있다
이번에는 Visual GC 탭을 확인해봤다
Q. 왜 Eden 영역의 사용량이 증가할까?
위에서 말했다시피 setLength(0)를 호출하더라도 StringBuilder의 내부 char[] 배열의 크기는 줄어들지 않는다(기존 메모리만 재사용)
하지만, 메모리가 꽉 차서 새로운 char[] 배열이 할당되는 경우 기존 배열이 사용되지 않게 돼서 메모리 사용이 증가할 수 있다
너무 사진만 첨부한 것 같아서 실제 이 프로그램으로 모니터링하면 어떻게 메모리가 변하는지 영상도 첨부한다
영상에 사용된 코드는 맨 처음 예제였던 불변 Integer를 Add하는 예시코드이다
이렇게 Visual VM을 통해서 Heap 메모리 사용량을 모니터링해보고, GC 카운트와 내부 영역도 눈으로 볼 수 있었다
유익한 시간이었길 바란다~!!
'Program Language > Java' 카테고리의 다른 글
자바에서의 다양한 문자열 포맷팅 방법(feat. MessageFormat) (0) | 2024.08.29 |
---|---|
자바에서 두 변수 값 바꾸기(Swap, Generic, Wrapper) (0) | 2024.08.24 |
Reference type(Mutable Object)의 Call By Value 살펴보기 2 (0) | 2024.08.23 |
댓글