Skip to main content

자바가상머신(JVM)에서 자바 메모리 관리

About 4 min

원글 : Java Memory Management for Java Virtual Machine (JVM)open in new window

자바 메모리 관리는 지속적인 과제이며 확장가능한 방식으로 작동하는 적절하게 조정된 어플리케이션을 갖추기 위해 숙달해야하는 기술입니다.
기본적으로, 새로운 객체를 할당하고 적절하게 미사용 객체를 삭제하는 과정입니다.

깊이 살펴보도록 하죠!

이번 글에서 자바가상머신(JVM)를 논의하고 메모리 관리, 메모리 모니터링도구, 메모리모니터링 사용법, 가비지컬렉터(GC)활동에 대해 이해해보겠습니다.

알다시피 진정한 최적화를 위한 많은 모델, 방법, 도구, 팁들이 있습니다.

자바가상머신(JVM)

JVM은 컴퓨터가 자바 프로그램을 실행할 수 있도록하는 추상적 컴퓨팅 장치이다.
JVM에는 3가지 개념을 가지고 있다.

  • 사양(Specification) : JVM이 동작하기 위한 사양입니다.
    실제 구현체는 Oracle이나 다른 기업에서 제공되고 있습니다.
  • 구현(Implementation) : 자바 런타임 환경(JRE)
  • 인스턴스(Instance) : 자바 명령이 작성된 뒤 자바 클래스를 실행하기 위해 JVM의 인스턴스가 생성됩니다.

자바가상머신은 코드를 불러오고 코드를 검증하고 코드를 실행하고 메모리를 관리(운영체제(OS)에 메모리 할당 / 힙 압축, 쓰레기 객체 삭제를 포함한 자바할당관리 포함) 하고 최종적으로 실행환경을 제공합니다.

자바(JVM) 메모리 구조

JVM 메모리는 다수의 부분으로 나뉘어져있습니다.

  • Heap 메모리
  • Non-Heap 메모리
  • Other 메모리

https://www.yourkit.com/docs/kb/jvm_memory_structure.gif

Heap 메모리

Heap 메모리는 모든 자바 클래스 인스턴스와 배열이 할당되는 메모리로 런타임 데이터 영역입니다.
Heap은 자바가상머신이 시작될 때 생성되고 어플리케이션 실행중에 크기가 증가/감소될 수 있습니다.
Heap의 최초크기는 -Xms VM 옵션으로 정의할 수 있습니다.
Heap은 가비지컬렉터 전략에 따라 고정된 크기 또는 동적 크기가 될 수 있습니다.
최대 Heap 크기는 -Xmx 옵션으로 설정됩니다.
기본적으로 최대 Heap 크기는 64 MB로 설정됩니다.

Non-Heap 메모리

자바가상머신은 Heap 이외의 Non-Heap 메모리를 가지고 있습니다.
이것은 JVM 시작할 때 생성되며 클래스당 구조체(런타임 상수풀, 필드, 함수 데이터, 함수와 생성자 코드, 고정 문자열과 같은 것)를 저장하고 있습니다.
Non-Heap 메모리의 기본 최대크기는 64 MB입니다.
-XX:MaxPermSize VM 옵션으로 변경할 수 있습니다.

기타 메모리

자바가상머신은 이 공간을 JVM 코드 자체, JVM 내부 구조체, 로딩된 프로파일러 에이전트 코드, 데이터 등을 저장하기 위해 사용합니다.

자바(JVM) Heap 메모리 구조

http://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java

JVM Heap은 물리적으로 두 파트(또는 세대)로 나뉘어집니다.

  • Nursery (또는 Young 영역/Young 세대)
  • Old 영역 (또는 Old 세대)

Nursery는 새로운 객체가 할당으로 예약된 Heap의 부분입니다.
Nursery가 가득차게되면, 특수한 Young 컬렉션을 실행하여 가비지가 수집됩니다.
Nursery에서 오랬동안 유지된 모든 객체가 Old 영역으로 상승(이동)하게 됩니다.
그래서 더 많은 객체 할당이 가능하도록 Nursery가 비워지게 됩니다.
이 가비지컬렉션은 마이너 GC라고 불립니다.
Nursery는 3개의 부분으로 나뉘게 됩니다.

  • Eden 메모리
  • 2개의 Survivor 메모리

Nursery 영역의 중요한 점은

  • 새로 생성되는 대부분의 객체는 Eden 메모리 영역에 위치합니다.
  • Eden 영역이 객체로 차게 되면, 마이너 GC가 수행되어지고 살아남은 모든객체는 Survivor 영역 중 하나로 이동하게 됩니다.
  • 마이너 GC 또한 Survivor 영역에서 살아남은 객체를 확인하고 이것을 다른 survivor 영역으로 이동시킵니다.
    이 순간 하나의 Survivor 영역은 항상 비어있게 됩니다.
  • 여러번의 GC 사이클에 살아남은 객체들은 old 세대 메모리 영역으로 이동하게 됩니다.
    보통, Nursery에 머물 수 있는 객체의 나이 한계점을 설정하고 이것을 토대로 old 세대로 상승할 자격을 검토하게 됩니다.

Old 세대가 가득차게 되면 가비지는 이것을 수집하고 이 프로세스를 Old 컬렉션이라고 합니다.
Old 세대 메모리는 여러번의 마이너 GC 후 길게 살아남은 객체들을 포함하고 있습니다.
보통, 가비지컬렉션은 Old 세대 메모리가 가득차면 수행됩니다.
Old 세대 가비지컬렉션은 메이저 GC라고 불리며 보통 오래걸리게 됩니다.
Nursery라는 이유는 대부분의 객체가 일시적이고 수명이 짧기 때문입니다.
Young 컬렉션은 아직 살아있는 새롭게 할당된 객체를 찾고 Nursery에서 밖으로 이동하는 것을 신속하게하도록 설계되어있습니다.
일반적으로, Young 컬렉션은 주어진 양의 메모리를 해지하는데 Old 컬렉션이나 단일 세대 Heap의 가비지컬렉션(Nursery가 제외된 Heap)보다 빠릅니다.

최근 릴리즈에는 Keep 영역이라는 Nursery의 일부가 포함되어 있으며 예약되어있습니다.
Keep 영역은 Nursery에 가장 최근 할당된 객체를 포함하고 있으며 다음 Young 세대까지 가비지로 수집되지 않습니다.
이것은 Young 컬렉션이 시작되기 바로 직전에 할당되어서 상승되는 객체를 방지할 수 있습니다.

자바 메모리 모델

영구 세대 (Java 8 이후 Metaspace로 교체됨)

영구 세대 또는 "Perm Gen"은 JVM에 요구되는 어플리케이션에서 사용되는 클래스와 함수를 서술한 메타데이터를 포함하고 있습니다.
Perm Gen은 어플리케이션에서 사용되는 클래스를 기반으로 런타임때 JVM에 의해 채워지게 됩니다.
Perm Gen은 또한 자바 SE 라이브러리 클래스 및 함수를 포함합니다.
Perm Gen 객체는 Full 가비지 컬렉션 때 수집되어 집니다.

Metaspace

자바 8에서는 Perm Gen이 없으며, "java.lang.OutOfMemoryError:PermGen" 공간 문제가 더이상 발생하지 않게 됩니다.
Perm Gen은 자바 Heap에 상주하는 것과 달리 Metaspace는 Heap 밖에 있습니다.
이제 대부분 클래스 메타데이터 할당은 네이티브 메모리 밖에 할당되어 집니다.
Metaspace는 항상 고정 최대크기를 가지는 Perm Gen과 다르게 기본적으로 자동으로 크기가 증가됩니다. (기본 OS가 제공하는 것까지)
Metaspace의 크기를 설정할 수있는 -XX:MetaspaceSize-XX:MaxMetaspaceSize라는 새로운 플래그가 있습니다.
Metaspace의 테마는 클래스와 메타데이터의 라이프타임은 클래스로더의 라이프타임과 일치하도록 되어있습니다.
즉, 클래스로더가 살아있다면 메타데이터는 Metaspace에 살아있도록 남아있으며 해지되지 않습니다.

코드 캐시

자바프로그램이 실행할 때 계층화된 방식으로 코드를 실행합니다.
첫번째 계층에서 코드를 기계어로 컴파일 하기 위해 클라이언트 컴파일러(C1 컴파일러)를 사용합니다.
두번째 계층에서 서버 컴파일러(C2 컴파일러)를 위한 프로파일링 데이터가 사용되며 코드를 최적화하여 컴파일할 수 있게 합니다.
계층화된 컴파일방식은 자바 7에서는 기본적으로 비활성화 되어있지만 자바 8부터는 활성화 되어있습니다.

함수 영역

함수영역은 Perm Gen의 일부 영역이며 클래스의 구조(런타임 상수 및 정적 변수), 함수와 생성자를 위한 코드를 저장하는데 사용됩니다.

메모리 풀

메모리 풀은 JVM 메모리 관리자에서 생성하며 불변의 객체 풀을 생성하는데 사용됩니다.
메모리 풀은 Heap 또는 Perm Gen에 속하며 JVM 메모리 관리자 구현체에 따라 다릅니다.

런타임 상수 풀

런타임 상수 풀은 클래스에 있는 상수풀이 런타임때 클래스별로 표현되는 것 입니다.
클래스 런타임 상수 및 정적 함수를 포함합니다.
런타임 상수 풀은 함수 영역의 일부입니다.

자바 Stack 메모리

자바 Stack 메모리는 쓰레드 실행에 사용됩니다.
짧게 생존하는 함수에 정의된 값과 함수에서 참조하는 Heap의 다른 객체에 대한 참조를 포함합니다.

자바 Heap 메모리 파라미터

자바는 메모리 크기와 비율을 설정할 때 사용할 수 있는 다양한 메모리 파라미터를 제공합니다.
일반적으로 사용되는 메모리 파라미터는 다음과 같습니다.

VM 파라미터VM 파라미터 설명
-XmsJVM이 시작될 때 초기 Heap 크기
-Xmx최대 Heap 크기
-XmnYoung 세대의 크기, 나머지는 Old 세대로 사용됨
-XX:PermGen영구 세대 메모리 초기 크기
-XX:MaxPermGenPerm Gen의 최대 크기
-XX:SurvivorRatioEden 공간의 비율, 예로들어 Young 세대가 10M이고 VM 파라미터에 -XX:SurvivorRatio=2로 하면 Eden 공간은 5M, 나머지는 2.5M으로 2개의 Survivor 공간이 예약됨. 기본값은 8
-XX:NewRatioOld/New 세대 크기 비율, 기본값은 2

가비지 컬렉션

가비지 컬렉션은 새로운 객체 할당을 위하여 Heap에 대한 공간확보를 하는 처리과정입니다.
자바의 가장 좋은 특징중에 하나는 자동화된 가비지 컬렉션 입니다.
가비지 컬렉터는 메모리에 있는 모든 객체를 관찰하며 프로그램의 일부에서 더이상 참조를 하지 않는 객체를 찾아내는 백그라운드에서 수행되는 프로그램 입니다.
이 모든 미참조되는 객체는 삭제되며 공간은 다른 객체의 할당을 위해 반환되어집니다.
가비지 컬렉션의 기본 방법중 하나는 다음 3단계를 가지고 있습니다.

  • 마킹(Marking) 이 첫번째 단계는 가비지 컬렉터가 사용되는, 사용되고 있지않은 객체를 식별하는 것 입니다.
  • 일반 삭제(Normal Deletion) 가비지 컬렉터가 미사용 객체를 삭제하고 다른 객체가 할당될 자유공간을 반환합니다.
  • 압축 삭제(Deletion with compacting) 더 나은 성능을 위해 미사용 객체가 삭제된 뒤 살아남은 모든 객체가 한곳으로 이동하게 됩니다. 이것은 새로운 객체를 위한 메모리 할당의 성능을 향상시켜줍니다.

가비지 컬렉션의 마크/스위프 모델

JVM은 전체 Heap에 대한 가비지 컬렉션을 수행하는데 마크/스위프 가비지 컬렉션 모델을 사용합니다.
마크/스위프 가비지 컬렉션은 두가지 단계로 구성되어있으며 마크 단계, 스위프 단계입니다.

마크 단계에서는 Java 쓰레드, 네이티브 핸들러, 다른 루트 소스에서 접근이 가능한 객체와 이 객체 또는 기타 등등이 접근가능한 다른 객체를 살아있다고 마킹합니다.
이 프로세스는 아직 사용중인 모든 객체를 식별하고 마킹하며 나머지는 가비지로 고려되어집니다.

스위프 단계에서는 Heap을 탐색하면서 살아있는 객체간의 갭을 찾게됩니다.
이 갭을 자유 목록에 기록을 하고 새로운 객체가 할당될때 사용가능하도록 만들어집니다.

자바 가비지 컬렉션 유형

어플리케이션에서 사용가능한 5가지 유형의 가비지 컬렉션이 있습니다.
어플리케이션의 가비지 컬렉션 전략을 활성화 하기위해 JVM 파라미터를 사용해야 합니다.

시리얼 GC (-XX:+UseSerialGC)

시리얼 GC는 Young과 Old 세대 가비지 컬렉션, 즉 마이너/메이저 GC를 위한 단순 마크-스위크-압축 접근을 사용합니다.

시리얼 GC를 활성화 하려면 다음 파라미터를 사용하면 됩니다.

-XX:+UseSerialGC
패러럴 GC (-XX:+UseParallelGC)

패러럴 GC는 시리얼 GC랑 비슷하지만 Young 세대 가비지 컬렉션을 수행할 때 N개의 쓰레드로 수행하는것이 다릅니다.
이때 N은 시스템의 CPU 코어수와 같습니다.
JVM의 옵션 중 -XX:ParallelGCThreads=n을 사용하여 쓰레드의 수를 조정할 수 있습니다.
이것은 JDK 8의 기본 컬렉터로 지정되어있습니다.

패러럴 GC를 활성화 하려면 다음 파라미터를 사용하면 됩니다.

-XX:+UseParallelGC
패러럴 Old GC (-XX:+UseParallelOldGC)

패러럴 GC와 동일하지만 Young 세대와 동일하게 Old 세대 가비지 컬렉션도 다수의 쓰레드를 사용하게 됩니다.

패러럴 Old GC를 활성화 하려면 다음 파라미터를 사용하면 됩니다.

-XX:+UseParallelOldGC
동시 마크/스위프(CMS) 컬렉터 (-XX:+UseConcMarkSweepGC)

CMS는 동시 순간 일시정지 컬렉터라고도 합니다.
이것은 Old 세대의 가비지 컬렉션을 수행합니다.
CMS 컬렉터는 대부분의 가비지 컬렉션을 어플리케이션 쓰레드와 동시에 수행하여 가비지 컬렉션에 대한 일시중지를 최소화하려고 합니다.
Young 세대의 CMS 컬렉터는 패러럴 컬렉터와 동일한 알고리즘을 사용합니다.
이 가비지 컬렉터는 일시정지되는 시간을 감당할 수 없는 반응형 어플리케이션에 적합합니다.
CMS 컬렉터의 쓰레드 수는 -XX:ParallelCMSThreads=n JVM 옵션을 사용하여 제한할 수 있습니다.

CMS 컬렉터를 활성화 하려면 다음 파라미터를 사용하면 됩니다.

-XX:+UseConcMarkSweepGC
G1 가비지 컬렉터 (-XX:+UseG1GC)

가비지 우선 또는 G1 가비지 컬렉터는 Java 7 부터 사용이 가능하며 CMS 컬렉터를 대체할 장기목표를 가지고 있습니다.
G1 컬렉터는 병렬/동시/증분압축이 가능한 순간 일시정지 가비지 컬렉터 입니다.
가비지 우선 컬렉터는 다른 컬렉터와 비슷하게 동작하지 않으며 Young, Old 세대 공간과 같은 컨셉이 없습니다.
이것은 Heap 공간을 다수의 동일크기 Heap 지역으로 나눕니다.
가비지 컬렉터가 수행될 때 살아있는 데이터가 적은 지역을 먼저 수집하게되어 "가비지 우선"이라고 합니다.

G1 컬렉터를 활성화 하려면 다음 파라미터를 사용하면 됩니다.

-XX:+UseG1GC

G1은 동시 마크-스위프 컬렉터(CMS)를 장기교체할 목적으로 계획되어있습니다.
G1과 CMS를 비교하면 G1이 좀더 나은 솔루션이라는 차이점이 있습니다.
한가지 다른점은 G1은 압축 컬렉터입니다.
G1은 할당을 위한 미세하게 나눠진 자유 목록을 완전히 사용하지 않고 대신 지역에 의존하여 충분히 압축합니다.
이것은 수집기의 일부를 상당히 단순하게 하고 대부분의 잠재적 파편화 이슈를 제거합니다.
또한, G1은 CMS 컬렉터보다 좀더 예측가능한 가비지 컬렉터 일시정지를 제공하며 사용자가 일시정지 대상을 지정할 수 있게 됩니다.

Java 8에서 G1 컬렉터가 문자열 중복제거로 알려진 놀라운 최적화와 함께 제공됩니다.
GC가 Heap에서 다량 발생되는 문자열을 식별하고 이것을 동일한 내부 char[] 배열을 가리키도록 변경하여 똑같은 복제본이 Heap에 없도록 합니다.
이것은 JVM 파라미터 중 -XX:+UseStringDeduplication으로 활성화 할 수 있습니다.

G1은 JDK 9부터 기본 가비지 컬렉터로 지정됩니다.

사용예시

자바 Heap의 50% 이상이 살아있는 데이터로 가득찰 때 수행됩니다.

객체할당 또는 승격에 대한 비율은 상당히 다양합니다.

가비지컬렉션 또는 압축에 대한 긴 일시정지는 지양하고 있습니다. (0.5 또는 1초 이상)

메모리 사용 및 GC 활동 모니터링

메모리 부족은 자바 어플리케이션에서 자주 불안정과 무반응의 원인이 될 수 있습니다.
따라서, 안정성과 성능 둘다 보장하기 위하여 응답시간과 메모리 사용량에 대한 가비지컬렉션의 영향을 모니터링해야합니다.
그러나, 메모리 사용과 가비지 컬렉션 타임에 대한 모니터링만으로는 부족하며, 두가지 요소만으로는 가비지 컬렉션이 어플리케이션의 응답시간에 영향을 준다고 말할 수 없습니다.
GC 중단만이 응답시간이 직접적인 영향을 주며 어플리케이션과 동시에 실행할 수 있습니다.
따라서 어플리케이션의 응답시간과 가비지 컬렉션에 의한 중단을 연관시켜야 합니다.
여기에 기반하여 우리가 모니터링해야할 것은 다음과 같습니다.

  • 다양한 메모리 풀(Eden, Survivor, Old 세대) 활용도. 메모리 부족은 GC 활동을 증가시키는 요인중 하나입니다.

  • 전체적 메모리 활용도가 가비지 컬렉션에도 불구하고 점진적으로 증가할 경우, 불가피하게 Out-Of-Memory로 이어지는 메모리 릭이 있다는 것 입니다.
    이 경우, 메모리 Heap 분석이 필요합니다.

  • Young 세대 컬렉션의 수는 이탈률 (객체 할당비율)에 대한 정보를 제공합니다.
    숫자가 높다면 더 많은 객체가 할당되고 있다는 것 입니다.
    Young 컬렉션의 높은 수치는 응답시간 문제와 커지는 Old 세대의 원인이 됩니다.
    (Young 세대는 더이상 객체의 양을 대처할 수 없기 때문입니다)

  • Old 세대의 활용도가 GC 이후 오르지 않고 크게 변동하는 경우, 객체가 불필요하게 Young 세대에서 Old 세대로 복사되어지고 있다는 것 입니다.
    이것의 세가지 가능한 원인이 있습니다: Young 세대가 너무 작고, 높은 이탈률이 있고, 트랜잭션 메모리 사용량이 너무 많다는 것 입니다.

  • 높은 GC 활동은 일반적으로 CPU 사용에 부정적 영향을 끼치게 됩니다.
    그러나, 중단(전체 이벤트 중지)만이 응답시간에 직접적인 영향을 가지고 있습니다.
    일반적인 의견과는 달리, 중단은 메이저 GC에 제한되지 않습니다.
    따라서 어플리케이션 응답시간에 상관관계가 있는 중단에 대한 모니터링이 중요합니다.

jstat

jstat 유틸리티는 수행중인 어플리케이션의 성능 및 자원소비에 대한 정보를 제공하기 위하여 자바 HotSpot VM의 내장 도구를 사용합니다.
성능 문제, 특히 Heap 크기나 가비지 컬렉션에 연관된 문제를 진단할 때 이 도구를 사용할 수 있습니다.
jstat 유틸리티는 VM을 실행하는데 특별한 옵션을 요구하지는 않습니다.
자바 HotSpot VM의 내장 도구는 기본적으로 활성화 되어있습니다.
이 유틸리티는 모든 운영체제에 대한 JDK를 다운받을 때 포함되어있습니다.
jstat 유틸리티는 대상 프로세스를 식별하기 위해 가상 머신 식별자 (VMID, Virtual Machine IDentifier)를 사용합니다.

JVM Heap 메모리 사용량을 찾기 위해 gc 옵션을 jstat과 함께 사용합니다.

<JAVA_HOME>/bin/jstat –gc <JAVA_PID>

https://www.betsol.com/wp-content/uploads/2017/06/java-home.jpg.webp

항목설명
S0C현재 Survivor 0 공간 용량 (KB)
S1C현재 Survivor 1 공간 용량 (KB)
S0USurvivor 0 공간 이용량 (KB)
S1USurvivor 1 공간 이용량 (KB)
EC현재 Eden 공간 용량 (KB)
EUEden 공간 이용량 (KB)
OC현재 Old 공간 용량 (KB)
OUOld 공간 이용량 (KB)
MCMetaspace 용량 (KB)
MUMetaspace 이용량 (KB)
CCSC압축된 Class 공간 용량 (KB)
CCSU압축된 Class 공간 이용량 (KB)
YGCYoung 가비지 컬렉션 이벤트 수
YGCTYoung 가비지 컬렉션 시간
FGC풀 가비지 컬렉션 이벤트 수
FGCT풀 가비지 컬렉션 시간
GCT전체 가비지 컬렉션 시간

jmap

jmap 유틸리티는 실행중인 VM 또는 코어 파일에 대한 메모리관련 통계를 출력합니다.
JDK 8에서 JVM과 자바 어플리케이션에 대한 문제진단을 위하여 Java Mission Control, Java Flight Recorder, jcmd 유틸리티를 소개하였습니다.
더 향상된 진단 및 감소된 성능 오버헤드를 위해 jmap보다는 최신의 유틸리티인 jcmd를 사용하는 것을 권장합니다.

-heap 옵션을 사용하면 아래의 자바 Heap 정보를 포함하게 됩니다.

  • GC 알고리즘 정보(GC 알고리즘 이름(예, 패러럴 GC)와 알고리즘에 특화된 상세정보(패러럴 GC의 쓰레드 수))
  • Heap 구성, 명령줄 옵션 또는 장치 구성에 기반하여 VM이 선택한 정보
  • Heap 사용 요약: 도구는 각 세대(Heap의 영역)마다 전체 Heap 수용량, 사용중인 메모리, 가능한 여유 메모리를 출력합니다.
    만약 세대가 공간의 집합체 (예, 새로운 세대)로 구성되어있다면, 공간에 특화된 메모리 크기 요약이 포함됩니다.
<JAVA_HOME>/bin/jmap –heap <JAVA_PID>

https://www.betsol.com/wp-content/uploads/2017/06/java-process.jpg.webp

jcmd

jcmd 유틸리티는 진단요청명령을 JVM에 보내는데 사용되며 이 요청은 Java Flight Recordings를 제어하고 트러블슈팅, JVM과 자바 어플리케이션을 진단하는데 유용합니다.
자바가상머신이 실행중이며 기동된 JVM이 사용하고 있는 동일한 사용자 및 그룹 식별자를 가지고 있는 동일한 장치에서 사용되어야 합니다.

Heap 덤프 (hprof dump)는 아래의 명령으로 생성가능합니다.

jcmd <JAVA_PID> GC.heap_dump filename=<FILE>

아래명령으로도 가능합니다.

jmap –dump:file=<FILE> <JAVA_PID>

다만 jcmd 명령을 더 권장합니다.

jhat

jhat 도구는 Heap 스냅샷에 대한 객체 토폴로지를 조회하는데 편리한 수단입니다.
이 도구는 Heap Analysis Tool (HAT)를 대체합니다.
도구는 바이너리 포맷으로 있는 Heap 덤프(예로 jcmd에 생성된 Heap 덤프)를 분석합니다.
이 유틸리티는 의도치않은 개체 관계를 디버그하는데 도움을 줍니다.
이 용어는 더이상 필요하지 않는데 루트집합에서 어떤 경로를 통하여 참조되기 때문에 살아있는 객체를 묘사하는데 사용됩니다.
예로 객체가 더이상 필요하지 않는데 의도하지 않는 정적 참조가 남아있는 경우, 옵저버나 리스너가 더이상 필요하지 않을 때 주제에서 자신을 등록취소하는데 실패한 경우, 객체를 참조하는 쓰레드에서 해야되는데 종료되지 않은 경우 발생됩니다.
의도치않은 객체 관계는 자바 언어에서 메모리 릭과 동일합니다.

아래의 명령으로 jhap을 사용하여 Heap 덤프를 분석할 수 있습니다.

jhat <HPROF_FILE>

이 명령은 .hprof 파일을 읽고 7000 포트로 서버를 시작합니다.

https://www.betsol.com/wp-content/uploads/2017/06/java-port-7000.jpg.webp

http://localhost:7000으로 서버에 접속하면 표준 쿼리를 실행하거나 객체 쿼리 언어(OQL, Object Query Language)를 생성할 수 있습니다.
모든 클래스 쿼리가 기본적으로 표시됩니다.
기본 페이지는 플랫폼 클래스를 제외하고 Heap에 있는 모든 클래스가 표시됩니다.
이 목록은 전체 표출된 클래스 명으로 정렬되어있으며 패키지별로 나눠져있습니다.
클래스의 이름을 클릭하면 클래스 쿼리로 이동합니다.
이 쿼리의 두번째 변형에는 플랫폼 클래스가 포함됩니다.
플랫폼 클래스는 java, sun, javax.swing과 같은 선두어로 시작되는 클래스를 포함합니다.
그러나, 클래스 쿼리는 클래스에 대한 정보를 표출합니다.
부모클래스, 자식클래스, 인스턴스 데이터 멤버, 정적 데이터 멤버 등을 포함합니다.
이 페이지로부터 참조되어지는 다른 클래스로 탐색하거나 인스턴스 쿼리로 탐색할 수 있습니다.
인스턴스 쿼리는 주어진 클래스의 모든 인스턴스를 표시합니다.

HPROF

HPROF는 모든 JDK 릴리즈에 탑재된 Heap과 CPU 프로파일링 도구입니다.
자바가상머신 도구인터페이스(JVMTI, Java Virtual Machine Tool Interface)를 사용하여 JVM과 연계하는 동적링크 라이브러리(DLL) 입니다.
도구는 프로파일링 정보를 파일 또는 ASCII/바이너리로 소켓에 기록합니다.
HPROF 도구는 CPU 사용량, Heap 할당 통계, 경합 프로파일 모니터링을 표출할 수 있습니다.
추가로 자바가상머신(JVM)의 완전한 Heap 덤프, 모든 모니터와 쓰레드에 대한 상태를 레포팅할 수 있습니다.
문제진단 측면에서 HPROF는 성능, 락 경합, 메모리 릭, 기타 이슈에 대한 분석을 할 때 도움이 됩니다.

HPROF 도구를 실행할 때 다음과 같이 하시면 됩니다.

java -agentlib:hprof ToBeProfiledClass

java -agentlib:hprof=heap=sites ToBeProfiledClass

프로파일링 요청 유형에 따라 HPROF는 연관된 이벤트를 전송하기 위해 JVM에 지시합니다.
그리고나서 도구는 이벤트 데이터를 프로파일링 정보로 처리합니다.
기본적으로, Heap 프로파일링 정보는 현재 작업중인 디렉터리의 java.hprof.txt(ASCII 방식)에 저장됩니다.

javac –J-agentlib:hprof=heap=sites Hello.java

위 명령은 Heap 할당 프로파일을 얻는데 사용됩니다.
Heap 프로파일의 중요한 정보는 프로그램의 다양한 부분에서 발생한 할당량 입니다.

비슷하게, Heap 덤프는 heap=dump옵션을 사용하여 포함할 수 있습니다.

javac –J-agentlib:hprof=heap=dump Hello.java

위 명령의 출력은 가비지 컬렉터에 의해 정의된 루트셋과 루트셋에서 접근이 가능한 Heap의 자바 객체 엔트리로 구성되어있습니다.

HPROF 도구는 샘플링 쓰레드로 CPU 사용량에 대한 정보를 수집할 수 있습니다.
아래 명령은 CPU 사용량 샘플링 프로파일 결과를 가져오는 것 입니다.

javac –J-agentlib:hprof=cpu=samples Hello.java

HPROF 에이전트는 가장 빈번하게 활성화되는 스택트레이스를 기록하기 위하여 주기적으로 모든 수행중인 쓰레드의 스택을 샘플링합니다.

그리고 GUI방식으로 메모리 사용량, 가비지컬렉션, Heap 덤프, CPU와 메모리 프로파일 등의 상세한 정보를 제공하는 VisualVM과 같은 도구도 있습니다.

VisualVM

VisualVM은 NetBeans 플랫폼에서 주도한 도구입니다.
아키텍쳐는 모듈형태로 되어있기 때문에 플러그인을 사용하여 확장하기 쉽습니다.
VisualVM은 자바 어플리케이션이 로컬 또는 원격 시스템의 JVM에서 실행될 때 상세한 정보를 가져올 수 있게 합니다.
생성된 데이터는 자바개발키트(JDK)를 사용해서 검색할 수 있으며, 여러 자바어플리케이션의 모든 데이터와 정보는 로컬 또는 원격 실행 어플리케이션 모두에서 빠르게 볼 수 있습니다.
또한 자바가상머신 JVM 소프트웨어의 데이터를 저장 및 캡쳐할 수 있고 로컬 시스템에 저장할 수 있습니다.
VisualVM은 CPU 샘플링, 메모리 샘플링, 가비지 컬렉션 수행, Heap 에러 분석, 스냅샷찍기 등을 할 수 있습니다.

JMX 포트 활성화

자바 어플리케이션을 시작할 때 아래의 시스템속성을 추가해서 JMX 원격 포트를 활성화할 수 있습니다.

  • -Dcom.sun.management.jmxremote
  • -Dcom.sun.management.jmxremote.port=<Port>
  • -Dcom.sun.management.jmxremote.

원격 머신에 접속하기 위해 VisualVM을 사용할 수 있으며 CPU 사용률, 메모리 샘플링, 쓰레드 등에 대한 정보를 볼 수 있습니다.
JMX 원격 포트를 통해 접속중인 원격머신에 대한 쓰레드 덤프, 메모리 덤프를 생성할 수 있습니다.

아래 그림은 로컬 및 원격시스템에 동작중인 어플리케이션에 대한 목록을 보여줍니다.
원격시스템에 접속하기 위해 "Remote"에 우클릭을 하고 호스트이름을 추가한 뒤 고급설정에서 원격머신에서 시작할 어플리케이션에 대한 포트를 정의하면 됩니다.
로컬 또는 원격 섹션에 나열된 어플리케이션에 대하여 더블클릭하면 상세정보를 볼 수 있습니다.

https://www.betsol.com/wp-content/uploads/2017/06/java-memory-management-2.jpg.webp

어플리케이션의 상세화면에서 4가지의 정보탭이 있습니다 : 오버뷰, 모니터, 쓰레드, 샘플러

오버뷰(Overview) 탭은 구동된 어플리케이션에 대한 주요 정보를 포함하고 있습니다.
메인 클래스, 명령줄 인자값, JVM 인자값, PID, 시스템 속성 및 쓰레드 덤프나 힙덤프 같은 저장된 데이터 등을 볼 수 있습니다.

가장 재미있는 것은 모니터(Monitor) 탭입니다.
이 탭은 어플리케이션의 CPU와 메모리 사용률을 보여줍니다.
이 뷰에는 4개의 그래프를 가지고 있습니다.

https://www.betsol.com/wp-content/uploads/2017/06/java-memory-management-3.jpg.webp

첫번째 그래프는 CPU 사용량, 가비지 컬렉터 CPU 사용량을 보여줍니다.
X-축은 Y-축의 이용률 대비 시간정보를 보여줍니다.

좌상단 두번째 그래프는 힙, Perm Gen, Metaspace 공간에 대한 정보를 표출합니다.
힙 메모리에 대하여 최대크기, 현재 사용량, 사용가능량 등을 표시합니다.
이 그래프는 부분적으로 java.lang.OutOfMemoryError: Java heap space 에러가 발생했을 때 어플리케이션에 대한 분석에 도움이 됩니다.

어플리케이션이 메모리에 관련된 동작을 수행할 때 사용된 힙(그래프상 파란영역)은 항상 힙크기(그래프상 주황색 영역)보다 작습니다.
사용된 힙이 힙크기와 같아지거나 시스템에 힙의 크기를 할당/확장할 공간이 없는 경우 그리고 사용된 힙이 계속 증가하게 될 경우 힙 에러가 예상됩니다.
힙에 대한 상세한 정보는 "힙 덤프"를 생성해서 확인할 수 있습니다.
VM 파라미터를 추가해서 Out-Of-Memory 오류가 발생할 때 힙덤프를 생성할 수 있게 할 수 있습니다.

-XX:+HeapDumpOnOutOfMemoryError –XX:HeapDumpPath=[file path]

위 옵션으로 특정 경로에 .hprof 파일을 생성할 수 있게 합니다.

https://www.betsol.com/wp-content/uploads/2017/06/java-memory-management-4.jpg.webp

위 그림은 특정 어플리케이션에 대한 힙 덤프를 보여줍니다.
요약 탭에서는 실행중인 어플리케이션에 대한 전체 클래스, 전체 인스턴스, 클래스로더, GC 루트, 환경과 같은 기본정보를 보여줍니다.
위 그림을 분석해보면 어떤 객체 타입이 가장 할당되었는지, 어디에서 할당이 발생했는지 볼 수 있습니다.
큰 객체는 생성자에서 다른 많은 객체를 생성하거나 다수의 필드를 가지고 있습니다.
또한 운영환경에서 대량으로 동시에 수행되는 알려진 코드영역도 분석해야합니다.
부하중에는 추가적인 할당뿐만 아니라 메모리 관리에 대한 동기화 또한 증가됩니다.
높은 메모리 사용은 과도한 가비지 컬레션에 원인이 됩니다.
몇몇의 경우 하드웨어 제한이 JVM의 힙사이즈에 대한 간단한 증가원인이 될 수 있습니다.
다른 경우에는 사용률이 꾸준히 증하기때문에 힙 사이즈를 키는 것만 으로는 해결이 되지 않고 지연만 시키는 경우도 있습니다.
Heap 덤프를 사용해서 아래와 같이 분석을 하면 메모리 누수나 메모리를 증가시키는 부분을 식별할 수 있습니다.

더이상 필요하지 않지만 어플리케이션에 참조되어 남아있는 모든 객체를 메모리 누수로 간주할 수 있습니다.
부분적으로, 메모리 누수에 관련하여 계속 증가하거나 너무 많은 부분을 찾이할 때만 고려할 수도 있습니다.
일반적인 메모리 누수는 특정 타입에 대한 객체를 계속 생성하지만 가비지로 수집되지 않았을 경우입니다.
이 객체 유형을 식별하려면 다수의 Heap 덤프를 가지고 트렌드를 분석하여 비교하는 것이 필요합니다.
모든 자바 어플리케이션은 대량의 String, char[] 와 다른 자바 표준 객체를 가지고 있습니다.
사실, String과 char[]는 일반적으로 정말 많은 숫자의 인스턴스를 가지고 있지만 그것들을 분석한다해서 유용한 결과를 가져오지는 못합니다.
String 객체에 누수가 발생해도 대부분은 본질적 누수의 원인이 되는 어플리케이션 객체에 참조되는 경우가 많습니다.
따라서 어플리케이션의 클래스에 집중하면 더 빠른 결과를 얻을 수 있습니다.

상세분석을 해야되는 몇가지 경우가 있습니다.

  • 트렌드 분석으로 메모리 누수를 찾지 못하는 경우
  • 어플리케이션이 많은 메모리를 사용하지만 분명한 메모리 누수가 없고 코드를 최적화해야될 경우
  • 메모리가 매우 빠르게 커지고 JVM이 크래시되는 것으로 트렌드 분석이 불가능한 경우

3개의 모든 경우 근본원인은 대부분 거대한 객체 트리의 루트에 한개 이상의 객체에 있습니다.
이 객체들은 트리에 있는 거의 대부분의 다른 객체에 대한 가비지컬렉션을 방지하게 됩니다.
Out-Of-Memory 에러의 경우, 소수의 객체로 인해 많은 객체가 해제되지 않아 Out-Of-Memory 에러가 발생할 수 있습니다.
힙의 크기는 자주 메모리 분석에 큰 문제를 가져옵니다.
힙 덤프 생성은 메모리 자체를 요구하고 있습니다.
만약 힙크기가 사용할 수 있는 또는 가능한 범위의 최대치라면(32-비트 JVM은 3.5GB 이상을 할당할 수 없음) 자바가상머신은 덤프를 생성할 수 없을 것 입니다.
추가로 힙덤프는 JVM을 중지시킬 것 입니다.
전체 객체트리에서 빠르게 가비지컬렉션되는 것을 방지하는 한 객체를 찾기는 거의 불가능한 일입니다.

다행이도 Dynatrace와 같은 솔루션이 이러한 객체를 자동으로 식별할 수 있게 합니다.
이를 위해 그래프이론에서 파생된 dominator 알고리즘을 사용해야합니다.
이 알고리즘은 객체트리의 루트를 계산할 수 있게 합니다.
객체트리 루트를 계산하는것에 더하여, 메모리 분석 툴이 특정 트리가 가지고 있는 메모리의 양을 계산합니다.
이 방법으로 어떤 객체가 고용량의 메모리를 해지에서 방지하고 있는지 계산할 수 있습니다.
다른말로 말해 어떤 객체가 메모리를 지배하는지 알 수 있습니다.

https://www.betsol.com/wp-content/uploads/2017/06/java-memory-management-5.jpg.webp

모니터 탭 아래에 있는 어플리케이션의 사용가능 그래프로 돌아가면 왼쪽 하단에 클래스 그래프가 있습니다.
이 그래프는 어플리케이션에 로드된 전체 클래스 숫자를 표시하고 마지막 그래프는 현재 수행중인 쓰레드의 숫자를 표시합니다.
이 그래프로 어플리케이션이 얼마나 많은 CPU나 메모리를 가지고 있는지 볼 수 있습니다.

다음 3번째 탭은 쓰레드(Threads) 탭 입니다.

https://www.betsol.com/wp-content/uploads/2017/06/java-memory-management-6.jpg.webp

쓰레드 탭에는 어플리케이션의 다른 쓰레드들이 상태가 변경되고 진화되는지 볼 수 있습니다.
또한 쓰레드의 각각 상태에 대한 소요된 시간과 다른 다양한 정보를 볼 수 있습니다.
살아있는 쓰레드나 종료된 쓰레드만 볼 수 있도록 필터링옵션도 있습니다.
쓰레드 덤프가 필요할 경우 상단의 "Thread Dump" 버튼을 사용해서 가져올 수 있습니다.

네번째 탭은 샘플러(Sampler) 탭 입니다.
초기에 이 탭을 열면 정보가 없습니다.
정보를 보기전에 먼저 샘플링/프로파일링과 같은 것을 시작해야합니다.
CPU 샘플링을 시작하기위해 "CPU" 버튼을 클릭하면 CPU 샘플링에 대한 결과가 테이블에 표시됩니다.

https://www.betsol.com/wp-content/uploads/2017/06/java-memory-management-7.jpg.webp

위 그림에서 doRun() 함수가 CPU 시간을 54.8% 사용했음을 볼 수 있습니다.
또한 getNextEvent()readAvailableBlocking() 이 두 함수가 다음으로 CPU 시간을 소비했음을 볼 수 있습니다.

다음 샘플링은 메모리 입니다.
어플리케이션은 결과가 처리될때까지 샘플링동안 일시정지됩니다.
아래 그림에서 어플리케이션이 저장하고 있는 Object, int, char 의 배열을 추론할 수 있습니다.

두가지 샘플링 유형에 대하여 나중에 사용할 수 있도록 파일로 결과를 저장할 수 있습니다.
예로들어, 샘플링을 일정한 간격으로 여러번 수행한 뒤 결과를 비교할 수 있습니다.
이것은 어플리케이션을 적은 CPU와 메모리를 사용하도록 개선하는데 도움을 줄 수 있습니다.
마지막으로 이 영역을 실험해보고 코드를 개선하는 것이 개발자의 역할입니다.

https://www.betsol.com/wp-content/uploads/2017/06/java-memory-management-8.jpg.webp

자바 가비지 컬렉션 튜닝

자바 가비지 컬렉션 튜닝은 어플리케이션의 처리량 증가와 긴 GC가 어플리케이션의 타임아웃에 원인이 되어 성능이 저하되는 것을 보았을때 사용되는 마지막 옵션입니다.

java.lang.OutOfMemoryError:PermGen space 에러에 직면하게 되면, JVM 옵션 중 -XX:PermGen-XX:MaxPermGen 을 사용하여 Perm Gen 메모리 공간에 대한 모니터링과 증설을 해야 합니다.
자바 8 이상의 경우 이 에러를 볼수는 없습니다.
다수의 Full GC 동작을 보게 된다면 Old 세대 메모리 공간을 증설해봐야 합니다.
전체적으로 가비지컬렉션 튜닝은 많은 시간과 노력이 필요하며 이에 대한 엄격하고 빠른 규칙은 없습니다.
다른 옵션들을 시도해보고 비교하여 어플리케이션에 최적화된 한가지를 찾아내야합니다.

몇가지 성능 해결책은 다음과 같습니다.

  • 어플리케이션 소프트웨어 샘플링/프로파일링
  • 서버와 JVM 튜닝
  • 알맞은 하드웨어와 OS
  • 어플리케이션과 샘플링 결과에 대한 코드 개선 (말보다 쉬운 방법!)
  • 자바가상머신을 알맞은 방법으로 사용하기 (최적화된 JVM 파라미터 사용)
  • 멀티프로세스일 경우 -XX:+UseParallelGC 사용하기

새겨두면 좋을 몇가지 유용한 팁도 있습니다.

  • 일시중지에 문제가 없다면 JVM에 할당가능한 만큼 메모리를 설정하기
  • -Xms-Xmx 를 같은 값으로 설정하기
  • 할당이 패러럴화 되도록 증가되는 프로세서의 수만큼 메모리를 증가시키기
  • Perm Gen 조정하는 것 잊지말기
  • 동기화 사용 최소화하기
  • 멀티쓰레드가 유용하고 쓰레드의 오버헤드를 알고 있는 경우 사용하기. 또한 다른환경에서 동일한 방식으로 동작하는지 확인하기
  • 사전 객체 생성을 피하세요. 생성은 사용되는 실제장소에서 가까이 있어야 합니다. 간과하기 쉬운 기본컨셉입니다.
  • JSP는 서블릿보다 보통 느립니다.
  • String concat 사용보다는 StringBuilder 사용하기
  • 기본유형을 사용하고 객체를 피하기 (Long 보다는 long)
  • 가능하면 객체를 재사용하고 불필요한 객체 생성 피하기
  • 공백 문자열을 equals() 로 테스트하기보다 length 속성을 사용하기
  • ==equals() 보다 빠릅니다.
  • n += 5n = n + 5보다 빠릅니다. 앞의 경우가 더 적은 바이트코드를 생성합니다.
  • 주기적으로 Hibernate 세션을 플러시와 지웁니다.
  • 수정과 삭제를 bulk로 수행하기

GC 로그 생성

가비지 컬렉터 로그, 또는 gc.log는 모든 JVM 메모리 정리 이벤트(마이너 GC, 메이저 GC, 풀 GC)가 저장된 텍스트 파일입니다.

아래의 JVM 파라미터를 사용하여 기동하면 gc.log를 생성하게 됩니다.

자바 8 까지

-XX:+PrintGCDetails -Xloggc:/app/tmp/myapp-gc.log

자바 9 이후

-Xlog:gc*:file=/app/tmp/myapp-gc.log