-
Notifications
You must be signed in to change notification settings - Fork 1
자바 애플리케이션 성능 및 메모리 관리(JVM)
- 강좌 링크: https://www.udemy.com/course/java-application-performance-and-memory-management/learn/lecture/13816100?
- the java 코드를 조작하는 다양한 방법: https://docs.google.com/document/d/11zgALhqn3igwfs4xc9cYdRPlyS84M6ba_6xz0I2M-Ik/edit
-
두 가지 관점에서 생각
- Memory constraints(메모리 제약)
- application speed(속도)
-
java 언어적인 측면이 아니라 JVM 실행 환경적인 측면에 대한 내용이 주를 이룰 것임
- 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 전달을 통해 확인할 수 있고 필요 시 외부 모니터링 툴과 연동할 수도 있음(그때그때 필요 시 찾아보면 되는 요소)
-
-
실행 시 여러 flag를 통해 컴파일 옵션을 줄 수 있다. => 튜닝 가능한 지점
-
java -XX:+PrintFlagsFinal : java 실행 시 지정할 수 있는 flag들을 보여준다.
-
성능에 영향을 줄 수 있는 요소(컴파일러 튜닝)
-
컴파일 프로세스를 실행하는 데 사용할 수 있는 스레드 수
- CICompilerCount flag
- jinfo -flag CICompilerCount [pid] 명령어를 통해 CICompilerCount의 기본 값을 알 수 있음
- 기본값은 3개
- [-XXLCICompilerCount=n] flag를 통해 실행되는 application의 컴파일 용 스레드 수를 지정할 수 있음
-
네이티브 컴파일의 임계값(동일한 메소드가 몇번 수행 될 때, 컴파일하는 것으로 판단 할 것인가?)
- [-XX:CompileThreshold=n] flag를 통해 실행되는 application의 임계 값을 지정 가능
- 기본값은 10000
-
- jvm은 바이트코드를 실행하는 표준
- 바이트코드는 기본적으로 인터프리팅 방식으로 실행 => 느림
- 자주 실행되는 코드 블락을 컴파일 방식으로 실행해서 성능 개선을 도모
- 튜님 요소
- 자주 실행되는 기준을(임계 값을) 튜닝
- 컴파일에 사용되는 스레드 수를 튜닝
- code cache를 위한 cache size를 튜닝
- 성능에 가장 영향을 미치는 메모리 요소는 GC
- GC를 이해하기 위해 JVM 메모리 동작 방식을 이해해야 한다.
- 또한, JVM 메모리 동작 방식은 프로그래밍을 통해 얼마든지 커스터마이징 할 수 있는 요소가 많다!
- (stack과 heap의 개념은 복습 측면이 강함)
- 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 인자값의 변경이 불가능합니다.
- final 필드:
-
사용 이유:
- 코드에 의도를 명확하게 하기 위해
- final 키워드를 사용하면 자바 컴파일러가 최적화 할 수 있는 여지가 생김(final 키워드를 쓰면 변경 불가하기 때문에 인라이닝을 할 수 있다)
-
const vs final
- 둘은 정확히 같은 개념? => 답은 아니다!
-
제목 | c++ const |
java final |
---|---|---|
선언과 동시에 초기화? | 초기화 해야 함 | 초기화 안해도 됨 |
객체의 참조의 경우 참조되는 객체의 상태를 변경하는 것이 가능? | 불가 | 가능 |
객체의 참조의 경우 참조되는 객체 자체를 바꾸는 것이 가능? | 가능 | 불가능 |
- 정리
- final변수의 의미는 한번만 참조를 지정할 수 있다는 것
- spring에서 autowired로 DI를 받을 변수도 final로 만들 수 있다.
- 즉, final변수가 참조하고 있는 객체의 상태는 언제든 바뀔 수 있다.
- final을 쓰면 컴파일러에 의해 선택적으로 인라인 처리가 될 수 있기 때문에 성능상의 이익을 얻을 수 있다(최적화 요소)
- final변수의 의미는 한번만 참조를 지정할 수 있다는 것
-
성능 보단 java code에서 실수를 많이 하는 요소와 관련한 chapter
-
escaping reference란?
- 결론부터 말하면 캡슐화 된 객체의 규칙을 깨는 행위임
- 즉, 캡슐화 되어 있는 필드에 대한 참조를 특정 메소드에서 리턴을 하는 등의 행위를 통해 그 참조 값이 외부로 직접 노출되는 것을 의미!
- private 필드의 참조를 외부에 직접 노출시키는 경우, 외부에서 private 필드의 참조 대상을 직접 접근 가능함으로 캡슐화를 위반하게 된다.
-
그럼 어떻게 escaping reference를 피할 수 있을까?
-
참조를 바로 노출하지 말고 iterator를 노출
- 근본적으로 escaping reference 해결하지는 않음
- 단지 개발자가 private 참조를 수정하기 좀 더 어렵개 할 수 있음(실수 유발 방지)
- 이 방법은 성능적 문제가 전혀 없음
-
참조를 복사 후 리턴
- iterator방식 보다 더 근본적인 해결책
- 복사 과정에서 약간의 오버헤드
-
참조를 복사 후 리턴(+ immutable한 복사본을 리턴)
- 이 방법도 마찬가지로 참조를 복사한 후 리턴하는 것
- 하지만 복사할 때 immutable한 방식으로 복사하는 것임
- 목적에 따라 리턴한 객체를 변경하고 싶지 않은 경우 이 방법을 사용하면 확실히 컴파일 타임에 오류를 잡을 수 있음
-
인터페이스 사용
- 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 정의 후 그걸 리턴
- 캡슐화를 목적으로 선언 되어 있는 필드 중
- Metaspace란?
-
우선은 metaspace와 GC는 무관하다고 이해하는게 맞는듯!
-
백기선님 강의의 메소드 영역을 의미하는 듯
-
추측으로는...
- jvm은 동적으로 클래스를 로딩한다.(즉, 런타임에 필요한 순간에 로딩)
- 로딩 될 때 클래스 정보는 Metaspace에 저장된다.
- 백기선님 사진 자료는 jvm 스팩을 정의해 둔 것일 뿐임 => 메소드 영역의 스팩을 구현한 것을 Metaspace라고 일단 이해하자
이게 맞는듯
-
- 정리
- jvm 스팩에는 method영역이 있다.
- 해당 스팩은 스팩일 뿐 너무 연연하지 말것! => 구현에 집중하자
- 즉, permGen => metaspace로의 이전에 대해서 집중하자
- 위 표에서 처럼
-
객체는 항상 힙에 생성될까?
- 우선, 기본적으로는 힙에 생성됨
- 하지만 하나의 메소드에서만 사용될 객체라면 사실, stack에 생성하는 것이 더 효율적일 텐데?(
GC를 안하고 알아서 객체가 없어질 것이니까
)이게 불가능한가? - 기본적으로 객체 생성을 어느 영역에서 할 지에 대한
개발자의 선택권은 java가 제공하지 않음
- 다만, 똑똑한 JVM이 알아서 분석해서 stack에 객체를 할당하는 경우는 있을 수 있음
-
string pool
- java에서 String은 immutable 한 객체임
- 따라서 다음과 같은 일이 발생
- 즉, 동일한 String 객체의 경우 힙에는 하나만 인스턴스화 되고 이에 대한 참조만 공유하는 방식을 java에서는 사용(성능상 이익)
- JVM은 일반적으로 할당되는 String 객체에 대해선 자체적인 String pool을 만들어 동일한 String 객체에 대한 참조만 제공
- 특별히 같은 내용의 다른 String 객체를 인스턴스화 하고 싶다면 새로운 String을 copy해야 한다.
- 이런 경우에서도 JVM이 선택적으로 String을 하나만 인스턴스화 하는 최적화를 만들 때도 있음!
- 또는 명시적으로 intern()을 호출해 copy가 생성되는 경우, copy를 만들지 않고 string pool에서 찾아보고 있으면 그걸 참조하도록 할 수 있음
- 이러한 string pool은 java 8 이상부턴 힙에 있다.(그 전에는 perm Gen에 있었음) => String pool도 GC 대상
- (heap에 있는) String pool의 실채
- string pool은 hashmap구조임(key값은 stringObject.getHashCode()로 갖는 hashmap)
- java hashmap은 buket으로 접근하고 동일한 buket의 원소는 이진트리로 최적화 되어 있음 => 검색에 최적화
- [string pool == hashMap]이라고 했을 때, hashmap의 버킷 숫자는 string pool검색 속도와 메모리 사용 량의 trade off가 있을 지점임
- => 튜닝 요소
...........
-
기본 개념
-
어떻게 java가 참조되지 않는 힙 객체를 알 수 있을까?
- stack으로 부터 이어지는 참조가 있는지 마킹하는 방식(unreachable object를 찾아 마킹하는 방식)
- 모든 heap 객체가 자신에 대한 참조 count를 가지는 방식
-
System.gc()
- JVM은 기본적으로 개발자가 메모리 관리를 하는 개념이 아님
- 하지만 System.gc()를 호출하면 일부 관여할 수는 있음
- 해당 메소드 콜은 gc를 반드시 하라는 command가 아니라 jvm에게 gc를 제안하는 정도로 이해할 것
- System.gc()를 사용하면 좋지 않은 이유
- 예측이 어렵다
- 기본적으로 gc는 system리소스를 많이 사용하게 되는데 이걸 JVM에게 위임 하지 않으면 왠만한 경우에서 비 효율적일듯..?
-
사용하지 않는 힙 메모리에 대한 GC의 메모리 반환
- java 11 이전
- 한번 할당된 사용 가능한 메모리 양은 결코 down되지 않음
- java 11 이후
- 상황에 따라 GC가 발생하면서 사용하지 않는 메모리를 OS에 반환한다.
- java 11 이전
-
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영역은 항상 비어있기 때문!
- 가장 일반적인 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된다.
- 이 임계값은 사용자가 설정할 수 있음 =>
튜닝 포인트
- 이 임계값은 사용자가 설정할 수 있음 =>
-
- major gc가 적게 일어날 수록 좋다는 관점으로 접근
- 해당 applicaton의 객체의 생명 주기에 맞춰서 튜닝을 해 줘야 한다.
-
[young gen | old gen]영역 조정
-
늘리면
- 장점: minor gc가 더 적게 발생 => young 객체들의 age가 천천히 올라감 => old gen이 느리게 차오름
- 단점: old gen영역이 줄어 듬 => 빨리 old gen이 바닥날 수 있음
-
줄이면
- 장점: 상대적으로 old gen영역 자체가 커짐
- 단점: minor gc가 더 많이 발생해서 old가 더 빨리 차긴 하는데?
-
상황에 따라 튜닝 방법이 다를듯...?
-
-
promotion의 임계 age count 조정
- 늘리면
- 장점: old gen이 천천히 찬다
- 단점: young gen에 메모리 부하가 갈 수 있을 듯
- 줄이면
- 장점: youn gen이 널럴해짐
- 단점: old gen이 빨리참
- 늘리면