ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바 Heap Dump
    JAVA공부/JVM 2022. 11. 4. 02:08

    JAVA 어플리케이션을 사용하다보면 발생할 수 있는 메모리 누수 혹은 Out Of Memory Error 문제,

    Permanent Full 문제를 분석해야할 때 Heap Dump 분석을 통해 해결점을 찾을 수 있다.

     

    Heap : 자바프로그램에서 참조형 데이터 타입을 갖는 객체(인스턴스), 배열, 메타정보 및 Object들에 대한

    참조 정보 등이 저장되는 메모리 공간이다.

    Dump : 기억 장치의 내용을 출력장치로 출력한다는 의미로 쓰인다.

     

    위에 대한 문제는 같은코드를 실행하더라도 모든 환경에서 항상 발생하는 것은 아니고

    개발환경에서는 잘 작동되지만 운영환경에서 데이터 & 환경 차이로 인해 발생 할 수 있다.

     

    만약 임의로 해당 Out Of Memory Error에러를 표출하고 싶으면

            List<byte[]> list = new ArrayList<>();
            int index = 1;
    		
            while (true) {
                // 1MB each loop, 1 x 1024 x 1024 = 1048576
                byte[] b = new byte[1048576];
                list.add(b);
                Runtime rt = Runtime.getRuntime();
                System.out.printf("[%d] free memory: %s%n", index++, rt.freeMemory());
            }

     

    와 같은 코드를 넣어서 큰메모리를 리스트에 담는 로직을 만들어서 무한 반복시킨다.

     

    저 코드를 실행하면 

    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 이 발생하게 된다.

     

    문제는 저렇게만 실행해버리면 덤프파일이 생성되지 않는다.

     

     

    파일생성 :: 자바 실행시 실행옵션 부과하기

    java -XX:+HeapDumpOnOutOfMemoryError **.java

    와같이 실행하면 OOM이 발생할 경우 힙덤프 파일(.hprof)을 생성해준다.

    -XX:HeapDumpPath를 추가하면 경로도 설정할 수있다.  

    만약 경로를 생략하면 JVM 시작경로에 파일이 생긴다.

     

    인텔리제이에서는 

     

    저기에서 Edit  Configuration에 들어간다.

     

     

    추가 옵션으로 VM options를 추가한다.

    -Xmx50m
    -Dfile.encoding=UTF-8
    -Dconsole.encoding=UTF-8
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=C:\Users\KIMTAEHYUN\Desktop

     

    다음과 같이 입력하였다.

     

    그리고 나서 작동을 하면 

     

    해당 파일이 생기는 것을 확인 할 수 있다.

    해당 파일의 용량은 72.7MB로 설정한 50MB의 JVM옵션보다 큰 것을 확인할 수 있었다.

     

    또 다른 방법으로는 HPROF 옵션을 사용하는 것인데, 해당 옵션은 JVM에서 제공하는 profiling 기능이며,
    제공하는 기능이 일반적인 heap dump를 하는것보다 많다.
    (CPU사용률, JAVA heap allocation상태, Thread 상태등을 표기해준다.)

     

     

    자바 기본 분석툴로 분석하기

    이제 파일을 생성했으니 분석툴로 분석해보자

     

    위에서는 OOM가 발생하였을 때 덤프를 떳지만,

     

    Jmap을 사용하면 콘솔에서 현재 실행 상황을 분석하거나 실행도중 덤프를 뜰 수 있고,

    Jhat을 사용하여 웹페이지에서 분석할 수 있다.

     

    - JPS 

    먼저 JPS 명령어를 콘솔에서 실행하면 실행중인 java 프로그램을 확인 할 수 있다.

     

    해당 프로세스 값이 무엇인지 확인하고 다음 jmap에서 사용을 해보자

    (나의 경우 2600)

     

    - Jmap 

    jmap -help를 치면 도움말이 나오는데 그중 나는 jmap -heap 2600 을 쳐서 해당 프로세스에 대해 힙덤프를

    하였다.

     

    그러면 위와 같은 내용이 나오는데 먼저 하나하나씩 분석해보자

     

     

    • Heap Configuration : 힙 메모리 설정에 대한 내용이 담겨 있다.
      • MinHeapFreeRatio :  %단위로 설정되는데, young  Generation(영역) 혹은 old Generation 영역이 해당 %보다 작을 경우 generation크기를 키워서 해당%이상으로 유지하게 한다. 현재 0이므로 작동하지 않는다.
      •  MaxHeapFreeRation : 이경우는 반대의 경우다 %단위로 설정하며 해당 %이상을 넘으면 크기를 줄여서 해당 %이하로 generation 크기를 줄인다. 현재 100%이므로 작동하지 않는다.
      • MaxHeapSize: 위에서 실행옵션으로 준 Heap메모리 사이즈이다.
      • New Size : Young generation 크기의 최소값 
      • MaxNewSize : Young generation 크기의 최대값 설정 (현재 둘다 16MB)
      • OldSize : Old generation의 크기
      • NewRatio : Young gen 과 Old gen의 상대적인 크기를 조절한다.
        현재 Old(33M) : New(16.5M) 2:1 이다.
      • SurvivorRatio : Young gen 안에 Eden과 Survivor0 Survivor1영역에 대한 비율이다.
                                 현재 Eden : Survivor[0,1]  =  8:1 이다.
      • MetaSpaceSize : 클래스 메타데이터에 사용되는 기본 메모리양
      • CompressedClassSpaceSize : 
      • MaxMetaSpaceSize : Perm 영역 Class,Method의 Meta정보 , Static변수 상수 저장되는 영역의 크기
      • G1HeapRegionSize : 하나의 G1 Region의 크기 설정
      • Young Generation : Young 영역  - 새로운 객체가 할당되는곳
        • Eden Space : Eden 영역
        • From Space : Survivor 1 영역
        • To Space : Survivor 2 영역
      • Old Generation : Old 영역 - 오랫동안 살아남은 객체가 저장되는 곳
                                                  (Young 영역에서 GC처리 중 Age 임계치에 도달하면 이동된다.)
                                               

    jmap -histo:live [PID] 명령으로으로 메모리 통계를 가져올 수도 있다.

     

    마지막으로 jmap -dump:format=b,file=test.hprof  [PID] 를 통해 실시간 덤프를 뜰 수도 있다.

     

    - Jhat

    위에서 만든 덤프 파일들 분석할 수 있는 JVM의 기본적인 툴이다.

    jhat -help 를 쳐보면 jhat을 실행하기 위한 jvm설정, 포트설정, 콜스택 추적에 대한 설정 등등이있는데,

    포트설정의 경우 생각 해주어야 하는것이 jhat은 웹서버를 통해 보여주기 때문에

    port가 7000(default)로 시작되게 된다. 따라서 해당 포트를 사용중이면 변경해주어야 하며,

    실제적으로 jhat [dump file] 명령어를 통해 실행하면

     

     

    와 같은 웹페이지를 확인 할 수 있으며,  여기서 show heap histogram을 확인하면 어느 클래스가 점유율이 가장 높은지

    순서대로 확인할 수 있다. (사실 jmap -histo 와 비슷하다.)

     

    또, 자주 해당 인스턴스가 어디서 발생하는지 찾아가기 위해

    해당 인스턴스명을 클릭하고 Exclude week refs를 클릭하면  

    상위 객체가 어느 패키지에서 어떤 필드명을 가지고 실행되었는지 확인 할 수 있다. 

     

     

    외부툴 사용 : IBM-heapanalyzer

    IBM HeapAnalyzer 을 통해 다운 받을 수 있으며,

    다운로드 후 실행하여 dump 파일을 실행하면

    과 같은 화면이 나온다.

     

     Analysis탭을 가보면 인스턴스들의 메모리 사용 비율, 메모리 누수 요소(추정)을 확인할 수 있다.

     

    그외에도 VisualVM, IDEA 플러그인 (이클립스 - MAT , IntelliJ - Profiling) 등이있고

     

    또 다른 옵션인 -verbose:gc 옵션을 주어서 GC의 처리과정을 파악할 수 있다.

     

    GC를 거쳐도 메모리 변화가 일어나지 않는 것을 관찰하여 OOM이 떨어지기전에 먼저 문제 발생유무를 파악할 수도 있을 것이다.

     

    해결하기

    1. Heap Dump로 유추한 메모리 누수 객체를 호출하는 곳에서의 로직적인 수정

     

    경험해본 것 

     

    - 자료구조 (컬렉션) 사용시 정적(static)필드로 사용되면서 계속 값이 추가 되는 경우

    (서버가 자주 재가동되면 발견하지 못하나 오랜시간 재가동하지 않으면 해당 문제가 생기는 것을 확인 할 수 있었다.)

     

    - Connection, Stream 사용시 객체를 닫아주지 않은 경우

    (IDE가 잘 되어있어 경고창이 뜨지만 해당 연결들을 닫지 않는 경우 JVM이 해당 인스턴스들을 GC대상으로 판별할 수 없다.)

     

     

    찾아본 것

     

    Understanding Memory Leaks in Java | Baeldung

    • 정적필드의 무분별한 사용 (위의 컬렉션과 같은 요소)
      - 정적 변수 사용을 최소화 한다.
      - 싱글톤 사용시에는 early load(클래스 로드 시점에 초기화)대신 lazy load(처음 사용 시점에 초기화)구현
      우선한다. 

    • 닫히지 않는 자원로부터 발생 (위에서 말한 Connection, Stream)
      - 항상 try catch 에서 finally 구문에 close 메소드를 실행한다.

    • equals()메소드와 hashCode() 구현이 잘못되어있는 경우 (HashSet, HashMap에서  - 키값으로 인스턴스 사용)
    public class Person {
        public String name;
        
        public Person(String name) {
            this.name = name;
        }
    }

    다음과 같은 Person 객체가 있는 경우

     

    @Test
    public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
        Map<Person, Integer> map = new HashMap<>();
        for(int i=0; i<100; i++) {
            map.put(new Person("jon"), 1);
        }
        Assert.assertFalse(map.size() == 1);
    }

    Map에서 Person 인스턴스를 키로 쓴다면, 해당 인스턴스는 같은 값을 가지고 있어도, 해당 Map에서 같은 키값이라고 인식을 하지 못한다. (hashCode()와 equals() override하지 않았기 때문)

     

    • 외부클래스를 참조하는 내부클래스 (non-static inner class를 사용하는 경우 발생한다.)
      응용프로그램에서 내부 클래스의 개체를 사용하면 이 내부클래스를 포함한 개체가 범위를 벗어난 후에도
      GC에 의해 처리 되지 않는다.
      - 내부 클래스가 엑세스 권한이 필요하지 않은 경우 정적 클래스로 전환

    • finalize() 메소드 사용시
      finalize() 메소드가 재정의 되면 , 해당 클래스는 바로 가비지에 수집되지 않는다. 대신에 나중에 종료하기 위해
      큐에 대기시킨다. 따라서 finalize() 큐가 가비지 수집기를 따라갈 수 없을 때 OOM을 만나게 된다.

      - finalize() 사용을 최대한 피한다.

    • 억류된 문자열
      거대한 문자객체를 읽고 inter()메소드를 실행할 때, 그것은 PermGen (permanent memory)에 있는
      String pool 로 이동한다. 그리고 애플리케이션이 가동되는 동안 그곳에 머물른다.

      - Java 7 이상 버전 사용 ( String pool 이 Heap space로 이동하였다.)
      - PermGen 영역의 크기를 늘린다. -XX:MaxPermSize=512m

    • ThreadLocal 사용하는 경우 (쓰레드 단위로 로컬 변수를 할당하는 기능)
      - remove() 메서드를 통해 현재 스레드값을 제거 (null 값 세팅 하는것을 말고 메서드 사용)

      - finally 블록에서 remove() 메서드 사용

    • 정말 메모리가 부족한 경우
      - GC 튜닝을 활용

     

    2. GC 튜닝

    우리가 여기서 주로 보아야할 것은 Old영역에대한 내용이다. 물론 Perm영역에서도 OOM이 일어날 수 있지만,

    애플리케이션 가동중에서 자주 발생하는 곳은 Old 영역이며, GC로 free한 메모리가 충분하게 공급되지 않기 때문이다.


    java 파일 실행시 다음과 같은 옵션을 수정하여, GC작업에 대한 개선을 할 수 있다.

    물론 메모리를 끝없이 증설,투자할 수 있는 환경에서는 비중이 작을 수도 있다.

    구분 옵션 옵션 설명
    힙영역 최소 크기 -Xms256m 시작시의 힙영역 크기이다. 
    애플리케이션 초기 가동시 필요한 메모리 이상으로 설정해야한다.
    힙영역 최대 크기  -Xmx512m 최대 힙 영역의 크기인데, 이 설정값에 
    따라 Old영역의 크기가 한정된다.
    New 영역과 Old 영역의 비율 -XX:NewRatio Young영역과 Old영역의 비율이다.
    초기값 1:2비율로 설정되지만,
    Old영역의 크기가 커야하는 경우
    조절 할 수 있지만, FullGC가 일어나면서
    애플리케이션이 멈추고, GC시간이 
    길어지는 것을 인지 해야한다.

     

     

     

     

    참고한 사이트 & 블로그들 : 

    Understanding Memory Leaks in Java | Baeldung

    [TroubleShooting] HeapDump 분석 가이드 (tistory.com)

    [Java] 자바 메모리 덤프 분석 - jps, jmap, jhat 사용법 및 예제 (tistory.com)

    'JAVA공부 > JVM' 카테고리의 다른 글

    GC 튜닝  (0) 2022.01.14
    Garbage Collection 모니터링  (0) 2022.01.12
    GC(Garbage Collection) 컬렉션 과 Heap 영역  (0) 2022.01.06
    JVM 기초 원리  (0) 2021.12.31
    JDK, JRE  (0) 2021.12.30
Designed by Tistory.