Skip to content

자바 애플리케이션 성능 및 메모리 관리(JVM)

rnjstjdgh edited this page Jun 1, 2021 · 49 revisions

단순 지식 뿐 아니라 이걸 실제로 활용해 최적화 할 수 있는 포인트에 관심을 갖자

Course Overview

  • 두 가지 관점에서 생각

    1. Memory constraints(메모리 제약)
    2. application speed(속도)
  • java 언어적인 측면이 아니라 JVM 실행 환경적인 측면에 대한 내용이 주를 이룰 것임


2. Just in time compiler와 코드 캐시

  • JIT 컴파일러
    • 기본적으로 JVM은 바이트코드를 인터프리팅 방식으로 실행

      • 바이트코드를 직접 실행하기 때문에 native보다 느림
      • 인터프리팅 방식은 컴파일 방식보다 느릴 수 밖에 없음(한줄 한줄 실행하기 때문에 전체를 보며 최적화가 어려움)
    • JVM은 실행 환경을 프로파일링 하면서 [자주 실행되는 | 복잡한 | 시간이 많이 걸리는] 블록을 [bytecode => native code]로 컴파일 하며 최적화를 한다.

    • 프로파일링 및 컴파일 과정은 JVM내의 별도의 스레드를 통해 수행된다.

      • 따라서, 코드 실행 스레드에는 영향을 주지 않음
    • JIT 컴파일러가 어떻게 동작하느냐에 따라 성능이 달라질 수 있다.

      • 어떤 부분을 언제, 어느 수준으로 컴파일 하고 언제 caching할지??
      • 컴파일 할 때에도 0(컴파일 안함)~4까지의 수준이 있다.(jvm은 프로파일링을 통해 얻은 데이터를 기반으로 이 수준을 결정)
      • 이러한 요소 중에 개발자 수준 커스텀 가능한 요소가 있을까?!
    • Tuning the code cache size

      • level 4로 컴파일 되어 케싱이 될 수 있는 요소가 많은 프로그램의 경우 caching되어 있는 code block이 가득 차 있으며 실행중 일 수 있다. 이러면 더는 새로운 caching 후보가 있어도 cache하지 못하는 경우가 발생한다.
      • cache의 max size와 지금 사용되고 있는 size를 알 수 있으며, 지금 사용 되고 있는 size가 max에 근접하다면 cache size 증가를 고려해 볼 만 하다.
    • cache size, 컴파일이 어떻게 일어나는지 등은 application 실행 시 특정 flag 전달을 통해 확인할 수 있고 필요 시 외부 모니터링 툴과 연동할 수도 있음(그때그때 필요 시 찾아보면 되는 요소)

3. JVM선택

  • 실행 시 여러 flag를 통해 컴파일 옵션을 줄 수 있다. => 튜닝 가능한 지점

  • java -XX:+PrintFlagsFinal : java 실행 시 지정할 수 있는 flag들을 보여준다.

  • 성능에 영향을 줄 수 있는 요소(컴파일러 튜닝)

    1. 컴파일 프로세스를 실행하는 데 사용할 수 있는 스레드 수

      • CICompilerCount flag
      • jinfo -flag CICompilerCount [pid] 명령어를 통해 CICompilerCount의 기본 값을 알 수 있음
      • 기본값은 3개
      • [-XXLCICompilerCount=n] flag를 통해 실행되는 application의 컴파일 용 스레드 수를 지정할 수 있음
    2. 네이티브 컴파일의 임계값(동일한 메소드가 몇번 수행 될 때, 컴파일하는 것으로 판단 할 것인가?)

      • [-XX:CompileThreshold=n] flag를 통해 실행되는 application의 임계 값을 지정 가능
      • 기본값은 10000

정리

  1. jvm은 바이트코드를 실행하는 표준
  2. 바이트코드는 기본적으로 인터프리팅 방식으로 실행 => 느림
  3. 자주 실행되는 코드 블락을 컴파일 방식으로 실행해서 성능 개선을 도모
  4. 튜님 요소
    1. 자주 실행되는 기준을(임계 값을) 튜닝
    2. 컴파일에 사용되는 스레드 수를 튜닝
    3. code cache를 위한 cache size를 튜닝

4. How memory works - the stack and the heap

  • 성능에 가장 영향을 미치는 메모리 요소는 GC
  • GC를 이해하기 위해 JVM 메모리 동작 방식을 이해해야 한다.
  • 또한, JVM 메모리 동작 방식은 프로그래밍을 통해 얼마든지 커스터마이징 할 수 있는 요소가 많다!

  • (stack과 heap의 개념은 복습 측면이 강함)

5. 메소드 간 객체 전달

  • call by value(primitive) vs call by reference(object) 복습
  • final 키워드 학습
  • 참고자료: https://djkeh.github.io/articles/Why-should-final-member-variables-be-conventionally-static-in-Java-kor/
    • 기본 개념: final의 의미는 최종적이란 뜻을 가지고 있습니다. final 필드는 초기값이 저장되면 최종적인 값이 되어 프로그램 실행 도중에 수정을 할 수 없습니다.

      • final 필드:
        • 값 초기화 후 변경 불가
      • final 객체:
        • 객체 변수에 final로 선언하면 그 변수에 다른 참조 값을 지정할 수 없습니다. 즉 한번 생성된 final 객체는 같은 타입으로 재생성이 불가능합니다. 객체자체는 변경이 불가능하지만 객체 내부 변수는 변경 가능합니다.
      • final 클래스:
        • 최종상태가 되어 더이상 상속이 불가
        • final 클래스여도 필드는 Setter함수를 통하여 변경은 가능합니다.
      • final 메서드:
        • 메서드에 final을 사용하게되면 상속받은 클래스에서 부모의 final 메서드를 재정의 할 수 없습니다. 자신이 만든 메서드를 변경할 수 없게끔 하고싶을때 사용되며 시스템의 코어부분에서 변경을 원치 않는 메서드에 많이 구현되어 있습니다.
      • 메서드 인자 값에 final 사용:
        • final 필드와 마찬가지로 인자값에 final을 사용하는 경우 final 인자값의 변경이 불가능합니다.
    • 사용 이유:

      1. 코드에 의도를 명확하게 하기 위해
      2. final 키워드를 사용하면 자바 컴파일러가 최적화 할 수 있는 여지가 생김(final 키워드를 쓰면 변경 불가하기 때문에 인라이닝을 할 수 있다)
    • const vs final

      • 둘은 정확히 같은 개념? => 답은 아니다!

제목 c++ const java final
선언과 동시에 초기화? 초기화 해야 함 image 초기화 안해도 됨 image
객체의 참조의 경우 참조되는 객체의 상태를 변경하는 것이 가능? 불가 image image 가능 image
객체의 참조의 경우 참조되는 객체 자체를 바꾸는 것이 가능? 가능 image 불가능 image

  • 정리
    1. final변수의 의미는 한번만 참조를 지정할 수 있다는 것
      • spring에서 autowired로 DI를 받을 변수도 final로 만들 수 있다.
    2. 즉, final변수가 참조하고 있는 객체의 상태는 언제든 바뀔 수 있다.
    3. final을 쓰면 컴파일러에 의해 선택적으로 인라인 처리가 될 수 있기 때문에 성능상의 이익을 얻을 수 있다(최적화 요소)

7. Escaping Reference

  • 성능 보단 java code에서 실수를 많이 하는 요소와 관련한 chapter

  • escaping reference란?

    • 결론부터 말하면 캡슐화 된 객체의 규칙을 깨는 행위임
    • 즉, 캡슐화 되어 있는 필드에 대한 참조를 특정 메소드에서 리턴을 하는 등의 행위를 통해 그 참조 값이 외부로 직접 노출되는 것을 의미!
    • private 필드의 참조를 외부에 직접 노출시키는 경우, 외부에서 private 필드의 참조 대상을 직접 접근 가능함으로 캡슐화를 위반하게 된다.
  • 그럼 어떻게 escaping reference를 피할 수 있을까?

    1. 참조를 바로 노출하지 말고 iterator를 노출

      • 근본적으로 escaping reference 해결하지는 않음
      • 단지 개발자가 private 참조를 수정하기 좀 더 어렵개 할 수 있음(실수 유발 방지)
      • 이 방법은 성능적 문제가 전혀 없음
    2. 참조를 복사 후 리턴

      • iterator방식 보다 더 근본적인 해결책
      • 복사 과정에서 약간의 오버헤드
    3. 참조를 복사 후 리턴(+ immutable한 복사본을 리턴)

      • 이 방법도 마찬가지로 참조를 복사한 후 리턴하는 것
      • 하지만 복사할 때 immutable한 방식으로 복사하는 것임
      • 목적에 따라 리턴한 객체를 변경하고 싶지 않은 경우 이 방법을 사용하면 확실히 컴파일 타임에 오류를 잡을 수 있음
    4. 인터페이스 사용

      • escaping reference 가 발생하는 지점에서 해당 객체의 read only interface를 만든 후 read only 타입으로 리턴 하는 방식
      • read only 타입에는 해당 인스턴스를 변경할 수 있는 어떤 메소드도 정의하지 않음
      • 이렇게 하면 클라이언트 코드에서도 리턴 타입의 의도가 명시적으로 확인이 됨
      • 또한 자연스럽게 escaping reference로 인한 문제상황도 발생하지 않음
      • 즉, escaping reference가 발생한 참조 객체에 대해 변경할 수 있는 모든 수단을 막아 둔 interface type으로 리턴을 시키는 것
      • 가장 우아한 해결책
      • 이 방법에서도 클라이언트 코드에서 구현체로 type casting 하게 되면 escaping reference가 발생 하긴 하나 이렇게 까지 클라이언트가 실수하지는 않을 것임..

  • 정리
    • 캡슐화를 목적으로 선언 되어 있는 필드 중 mutable한 객체의 직접적인 레퍼런스가 객체 바깥으로 세어 나갈 수 있는 것을 escaping reference라 함
    • 주로 mutable한 객체의 레퍼런스를 리턴하는 메소드를 통해 escaping reference가 발생
    • immutable한 타입의 경우는 이러한 문제가 발생하지 않는다
    • escaping reference 문제의 해결책
      • immutable한 객체의 복사본 리턴 or read only interface 정의 후 그걸 리턴

9. The Metaspace and internal JVM memory optimisation


  • 정리
    1. jvm 스팩에는 method영역이 있다.
    2. 해당 스팩은 스팩일 뿐 너무 연연하지 말것! => 구현에 집중하자
    3. 즉, permGen => metaspace로의 이전에 대해서 집중하자
      • image
      • 위 표에서 처럼

  • 객체는 항상 힙에 생성될까?

    • 우선, 기본적으로는 힙에 생성됨
    • 하지만 하나의 메소드에서만 사용될 객체라면 사실, stack에 생성하는 것이 더 효율적일 텐데?(GC를 안하고 알아서 객체가 없어질 것이니까)이게 불가능한가?
    • 기본적으로 객체 생성을 어느 영역에서 할 지에 대한 개발자의 선택권은 java가 제공하지 않음
    • 다만, 똑똑한 JVM이 알아서 분석해서 stack에 객체를 할당하는 경우는 있을 수 있음
  • string pool

    • java에서 String은 immutable 한 객체임
    • 따라서 다음과 같은 일이 발생
      • image
      • 즉, 동일한 String 객체의 경우 힙에는 하나만 인스턴스화 되고 이에 대한 참조만 공유하는 방식을 java에서는 사용(성능상 이익)
    • JVM은 일반적으로 할당되는 String 객체에 대해선 자체적인 String pool을 만들어 동일한 String 객체에 대한 참조만 제공
    • 특별히 같은 내용의 다른 String 객체를 인스턴스화 하고 싶다면 새로운 String을 copy해야 한다.
      • image
      • 이런 경우에서도 JVM이 선택적으로 String을 하나만 인스턴스화 하는 최적화를 만들 때도 있음!
      • 또는 명시적으로 intern()을 호출해 copy가 생성되는 경우, copy를 만들지 않고 string pool에서 찾아보고 있으면 그걸 참조하도록 할 수 있음
    • 이러한 string pool은 java 8 이상부턴 힙에 있다.(그 전에는 perm Gen에 있었음) => String pool도 GC 대상

10. JVM의 메모리 설정 조정

  • (heap에 있는) String pool의 실채
    • string pool은 hashmap구조임(key값은 stringObject.getHashCode()로 갖는 hashmap)
    • java hashmap은 buket으로 접근하고 동일한 buket의 원소는 이진트리로 최적화 되어 있음 => 검색에 최적화
    • [string pool == hashMap]이라고 했을 때, hashmap의 버킷 숫자는 string pool검색 속도와 메모리 사용 량의 trade off가 있을 지점임
      • => 튜닝 요소

...........

11. GC 소개

  • 기본 개념

  • 어떻게 java가 참조되지 않는 힙 객체를 알 수 있을까?

    1. stack으로 부터 이어지는 참조가 있는지 마킹하는 방식(unreachable object를 찾아 마킹하는 방식)
    2. 모든 heap 객체가 자신에 대한 참조 count를 가지는 방식
  • System.gc()

    • JVM은 기본적으로 개발자가 메모리 관리를 하는 개념이 아님
    • 하지만 System.gc()를 호출하면 일부 관여할 수는 있음
      • 해당 메소드 콜은 gc를 반드시 하라는 command가 아니라 jvm에게 gc를 제안하는 정도로 이해할 것
    • System.gc()를 사용하면 좋지 않은 이유
      • 예측이 어렵다
      • 기본적으로 gc는 system리소스를 많이 사용하게 되는데 이걸 JVM에게 위임 하지 않으면 왠만한 경우에서 비 효율적일듯..?
  • 사용하지 않는 힙 메모리에 대한 GC의 메모리 반환

    • java 11 이전
      • 한번 할당된 사용 가능한 메모리 양은 결코 down되지 않음
    • java 11 이후
      • 상황에 따라 GC가 발생하면서 사용하지 않는 메모리를 OS에 반환한다.
  • finalize method

    • java 9부터 deprecate됨
    • 왜?
      • https://brunch.co.kr/@oemilk/122 참고
      • finalize는 GC로 인해 객체가 소멸되기 직전에 수행됨
        • GC와 밀접한 관련
        • GC는 전적으로 JVM의 권한
        • 따라서 user code가 finalize method에 들어가는 것은 말이 안됨
        • finalized method에 무한 루프를 넣으면 GC Thread가 해당 메소드에서 무한루프에 빠짐 => GC 장애 발생 (더군다나 finalize method에선 에러도 발생이 안됨)
  • java에선 memory leak이 불가능하다?

    • 우선 기본적으로 GC는 unreachable한 객체를 대상으로 발생한다.
    • 사용하지는 않지만 참조되고 있는 객체가 지속적으로 늘어나게 되면 약간의 메모리 누수 발생(이를 soft leak이라고 함)
    • 즉, 참조하고 있지만 사용하고 있지 않은 객체가 런타임에 지속적으로 생길 경우 메모리 누수가 발생할 수 있다.
    • soft leak의 경우에도 힙 사용량이 힙 max size를 넘어가는 일은 없다.
      • because> survivor 0 또는 survivor 1영역은 항상 비어있기 때문!

14. GC 개념

  • 가장 일반적인 GC => Mark & Sweep
    • Mark하는 과정에서 stop the world 발생

    • 사용중인 객체가 많을 수록 stop the world 시간이 길어짐

    • 따라서 사용중인 객체를 generation별로 관리

      • 전체를 대상으로 항상 GC를 수행하는 것 보다 구간을 나눠서 선택적으로 하는것이 더 효율적
      • 특히, 일반적으로 한번의 GC에서 살아남은 객체는 다음 GC에서도 살아남을 가능성이 높음
        • 이런 객체를 old gen으로 보내고 빨리 소멸될 객체를 young gen으로 분류하면 결국 빨리 소멸될 객체와 아닌 객체를 구분할 수 있는 효과
        • 어차피 안 없어질 가능성이 높은 객체에 대해 자꾸만 GC check를 할 필요가 없어짐
        • 또한, young gen의 객체는 대부분 빨리 unreachable상태가 되기 때문에 mark 과정이 효율적으로 발생하게 됨
    • young gen을 또 3등분 한 것도 minor GC를 효율적으로 하기 위함임

      • 실제로 minor gc는 young gen의 2/3만 바라보고 발생하게 되어서 더 효율적
    • minor gc

      • eden영역이 가득 찰 때 마다 발생
      • 발생할 때 마다 살아남은 객체를 s0 또는 s1으로 이동(둘중 하나는 반드시 비어있어야 한다)
      • stop the world에 대한 오버해드가 적다
    • major gc

      • old gen영역이 가득 찰 때 마다 발생
      • stop the world에 대한 오버해드가 크다
      • gc해야 하는 heap영역(old gen)자체가 크고 대부분 reachable일 가능성이 높기 때문
      • soft leak이 발생할 경우 결국 참조되고 있는 객체의 age가 증가하여 old gen에 누적될 것이고 old gen에 gc를 아무리 해도 이것들은 reachable하기 때문에 GC대상이 아니다
        • 이러한 이유로 old gen이 항상 가득 차게 되고 major gc가 계속 발생하게 되어 성능에 치명적인 영향을 줄 것임!
    • GC가 될때 살아남은 객체는 age가 하나씩 증가한다.

    • age가 일정 수준 이상이 된 객체는 old gen으로 promotion된다.

      • 이 임계값은 사용자가 설정할 수 있음 => 튜닝 포인트

15. GC 튜닝 및 선택

  • major gc가 적게 일어날 수록 좋다는 관점으로 접근
  • 해당 applicaton의 객체의 생명 주기에 맞춰서 튜닝을 해 줘야 한다.
  1. [young gen | old gen]영역 조정

    • 늘리면

      • 장점: minor gc가 더 적게 발생 => young 객체들의 age가 천천히 올라감 => old gen이 느리게 차오름
      • 단점: old gen영역이 줄어 듬 => 빨리 old gen이 바닥날 수 있음
    • 줄이면

      • 장점: 상대적으로 old gen영역 자체가 커짐
      • 단점: minor gc가 더 많이 발생해서 old가 더 빨리 차긴 하는데?
    • 상황에 따라 튜닝 방법이 다를듯...?

  2. promotion의 임계 age count 조정

    • 늘리면
      • 장점: old gen이 천천히 찬다
      • 단점: young gen에 메모리 부하가 갈 수 있을 듯
    • 줄이면
      • 장점: youn gen이 널럴해짐
      • 단점: old gen이 빨리참