Skip to content

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

rnjstjdgh edited this page May 19, 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변수의 의미는 한번만 참조를 지정할 수 있다는 것
    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

  • Metaspace란?

    • 백기선님 강의의 메소드 영역을 의미하는 듯
    • image
    • image
  • 객체는 항상 힙에 생성될까?

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