도서 - 자바 최적화 1
by choising
자바 최적화 (~ 187p)
들어가며
-
지금이 자바 개발자들에게 가장 흥분되는 시기 아닐까요? 지금처럼 자바 플랫폼으로 다재다능하고 응답성 좋은 애플리케이션을 구축할 기회가 많았던 적은 한번도 없었습니다. 건투를 빕니다!
CHAPTER 1. 성능과 최적화
- 자바 초창기에는 메서드 디스패치 성능이 최악이었다
- 메서드를 잘게 나누지 말고, 하나의 덩치 큰 메서드로 작성하는게 좋다고 권고하는 개발자가 있었다고 한다. 이 때는 성능 »»»> 넘사 »» 가독성 이었을듯
- 최근은 JVM이 넘나 좋아졌고, 자동 인라이닝(automatic managed inlining) 덕분에 그런 문제가 없다고 한다
- 자동 인라이닝이 뭐임?
- 찾아보니
-
Inlining은 소형 메소드 트리가 해당 호출자 트리로 병합되거나 “인라인되는” 프로세스입니다. 이렇게 하면 자주 실행되는 메소드 호출의 속도가 향상됩니다. - 출처 갓IBM
- 위 글을 보면 JIT(just-in-time) 컴파일러가 컴파일 시 1번 과정이 inlining 이라고 한다
- 이렇게 하면 메소드 여러번 호출 시 메소드 로직을 실제로 여러번 돌리는게 아니라 한 번 돌려서 나온 값을 바로 쓸 수 있게 되는 것이고 그러면 성능이 올라갈 것 같긴 하다고 이해했음
- 인터넷에서 찾은 글을 무턱대고 믿어선 안된다고 한다
- 하지만 어쩔수 없는걸..
- 이런 이유로 이 책에서는 코드에 바로 써먹을 수 있는 성능 팁을 나열하진 않았다고 한다
- 여러가지 단면을 종합 집중 조명하겠
1.2 자바 성능 개요
- 자바는 실용적인 언어.
- 관리되는 서브시스템(managed subsystem), 개발자가 일일히 용량을 세세하게 관리하는 부담을 덜어주고, 대신 저수준으로 제어 가능한 일부 기능을 포기하자는 발상.
- 예 ) GC
- JVM 애플리케이션의 성능 측정값은 정규분포를 따르지 않는 경우 가많다.
- 샘플링 해버리면 특이점이 묻혀버릴 수 도
소프트웨어 산업의 가장 경이적인 성과는 하드웨어 산업에서 꾸준히 이루어낸 혁신을 끊임없이 무용지물로 만들고 있는 것이다. - 헨리 페트로스키
- ㅋㅋ 이거 개웃기고 팩트다. 하드웨어 싸고 성능 좋아서, 성능보다 갓독성을 중시하니깐
- 소프트웨어 성능을 정량적으로 측정할 수 없다. 즉
성능
은 아래와 같은 활동을 하면서 원하는 결과를 얻기 위한, 일종의실험과학
이다.- 원하는 결과 정의
- 기존 시스템 측정
- 요건 충족하기 위해 무슨일을 해야할 지 정리
- 개선활동 추진
- 테스트
- 목표 달성 ? 끝 : 다시
1.4 성능 분류
- 가장 일반적인 기본 성능 지표
- 처리율
- 시스템이 수행 가능한 작업 비율
- 일정 시간 동안 완료한 작업 단위수 (예 : TPS)
- 지연
- 하나의 트랜잭션을 처리하는데 소요된 시간
- 종단 시간
- 용량
- 시스템이 동시 처리 가능한 작업 단위(트랜잭션) 개수
- 딱 봐도 처리율과 밀접한 연관
- 사용률
- 사용 목적이나 리소스별로 들쑥날쑥할 수 있다
- 효율
- (처리율 / 리소스 사용률)
- 가격으로 측정하는 방법도 있음
- 확장성
- 리소스 추가에 따른 처리율 변화로 확장성을 가늠할 수 있음
- 클러스터를 2배로 늘려 트랜잭션 처리량도 2배 늘었다면 이 시스템은 완벽한 선형 확장
- 현실적으로 이럴 수는 없음
- 저하
- 요청 개수 증가, 요청 접수 속도 증가 등 어떤 형태로든 시스템이 더 많은 부하를 받으면 지연 / 처리율 측정 값에 변화가 생긴다.
- 사용율이 낮았으면 측정값이 느슨하게 변하고, 시스템이 풀 가동된 상태라면 처리율이 더는 늘지 않고 지연이 증가하는 양상. 이런 현상을 부하 증가에 따른 저하
- 처리율
- 실제로 어느 한 지표를 최적화 하면 다른 지표(들)가 악화되는 경우도 흔하다
CHAPTER 2. JVM 이야기
2.1 인터프리팅과 클래스로딩
- VM Spec : 자바 가상머신 규정 명세서
- JVM은 스택 기반의 해석 머신
- 물리적 CPU 하드웨어인 레지스터는 없지만, 일부 결과를 실행 스택에 보관하며, 이 스택의 맨 위에 쌓인 값(들)을 가져와 계산한다
- JVM 인터프리터의 기본 로직
- 쉽게 말하자면, 평가 스택을 이용해 중간값들을 담아두고 가장 마지막에 실행된 명령어와 독립적으로 프로그램을 구성하는 opcode(명령어)를 하나 씩 순서대로 처리하는
while 루프 안의 switch 문
- 쉽게 말하자면, 평가 스택을 이용해 중간값들을 담아두고 가장 마지막에 실행된 명령어와 독립적으로 프로그램을 구성하는 opcode(명령어)를 하나 씩 순서대로 처리하는
- 클래스 로딩
- 부트스트랩 클래스가 자바 런타임 코어 클래스 로드
- 확장 클래스 로더 생김, 부트스트랩 클래스로더가 자기 부모고, 필요할 때 클래스로딩 작업을 부모에게 넘긴다
- 끝으로, 애플리케이션 클래스로더가 생성되고 지정된 클래스패스에 위치한 유저 클래스를 로드한다.
- 자바는 프로그램 실행 중 처음보는 새 클래스를 디펜던시에 로드.
- 찾지 못한 클래스로더는 자신의 부모 클래스로더에게 룩업을 넘김
- 결국 부투스트랩도 룩업하지 못하면 ClassNotFoundException 이 나게됨.
- 한 시스템에서 클래스는 패키지명을 포함한 풀 클래스 명과 자신을 로드한 클래스로더, 두 가지 정보로 식별된다.
2.2 바이트 코드 실행
- 바이트 코드는 특정 컴퓨터 아키텍처에 특정하지 않은
중간 표현형 (IR)
- 컴퓨터 아키텍처의 지배 ㄴㄴ
- 이식성 좋
- 개발을 마치고 컴파일 된 소프트웨어
- 클래스 파일
- 매직 넘버 : 0xCAFEBABE
- 클래스 파일 포맷 버전 : 메이저/마이너
- 상수 풀 : 클래스 상수들이 모여 있는 위치
- 액세스 플래그 : 추상 클래스, 스테틱 클래스 등 클래스 종류 표시
- public 인지 final 인지, 인터페이스인지, 추상 클래스인지
- this 클래스 : 현재 클래스
- super 클래스 : 수퍼 클래스
- 인터페이스 : 클래스가 구현한 모든 인터페이스
- 필드 : 클래스에 들어 있는 필드
- 메서드 : 클래스에 들어 있는 메서드
- 속성 : 클래스가 지닌 모든 속성 (소스 파일명 등)
2.3 핫스팟 입문
- JIT 컴파일
- 바이트코드 -> 네이티브 코드로 컴파일
- 핫스팟은 인터프리티드 모드로 실행하는 동안 애플리케이션을 모니터링하면서 가장 자주 실행되는 코드 파트를 발견해 JIT 컴파일을 수행
- 특정 메서드가 어느 한계치를 넘어가면 프로파일러가 특정 코드 섹션을 컴파일/최적화 한다
- 컴파일러가 해석 단계에서 수집한 추적 정보를 근거로 최적화를 결정한다는게 가장 큰 장점
2.4 JVM 메모리 관리
- 자바는 Garbage Collection 이라는 프로세스를 이용해 힙 메모리를 자동 관리하는 방식
- JVM이 더 많은 메모리를 할당해야 할 때 불필요한 메모리를 회수하거나 재사용하는 불확정적(nondeterministic) 프로세스
- Stop The World
2.6 JVM 구현체 종류
- OpenJDK : 오픈소스, 오라클이 직접 프로젝트를 주관/지원
- 오라클Java
- Zulu(줄루) : 유료 지원 서비스 가능
- 아이스티(IcedTea) : 레드햇
- J9 : IBM
2.7 JVM 모니터링과 툴링
- Java Management Extensions(JMX) : 자바 관리 확장
- JVM과 그 위에서 동작하는 애플리케이션을 제어하고 모니터링하는 강력한 범용 툴
- 자바 에이전트
- 자바 언어로 작성된 툴 컴포넌트로, java.lang.instrument 인터페이스로 메서드 바이트코드를 조작
Chapter 3. 하드웨어와 운영체제
- 성능을 진지하게 고민하는 자바 프로그래머는 가용 리소스를 최대한 활용할 수 있도록 자바 플랫폼의 근간 원리와 기술을 잘 알고 있어야 한다
3.2 메모리
- 초기 트랜지스터는 클락율 향상에 집중했다
- 시간이 갈수록 프로세서 코어의 데이터 수요를 메인 메모리가 맞추기 어려워졌다
- 즉,
프로세서/메모리 간 성능 차이가 발생
- 클락율이 아무리 올라가도 데이터가 도착할 때 까지 CPU는 유휴
3.2.1 메모리캐시
- 그래서 CPU 캐시 등장(
캐시 아키텍처
) - 레지스터보다는 느리고 메모리보다는 빠른, CPU에 있는 메모리 영역
- 자주 액세스 하는 거는 CPU가 메인메모리를 재참조하지 않게 사본을 떠서 CPU 캐시에 보관하자는 아이디어
- 액세스 빈도가 높은 캐시일 수록 프로세서 코어와 더 가까이 위치 하는 식으로 여러 캐시 계층 존재
- L1 : 가장 가까운 캐시
- L2, L3..
- 일반적으로 각 실행 코어에 전용 프라이빗 캐시 L1, L2를 두고, 일부 또는 전체 코어가 공유하는 L3 캐시를 둠
- 최신 CPU는 더 많은 예산을 캐시에 투자
- 캐시 아키텍처로 프로세서 처리율은 개선되었지만, 다른 문제
- 메모리에 있는 데이터를 어떻게 캐시로 가져오고, 캐시한 데이터를 어떻게 메모리에 다시 써야 할 지
- 캐시 일관성 프로토콜
- MESI 프로토콜
- 캐시 라인(보통 64바이트) 상태를 다음 네 가지로 정의
- Modified(수정) : 데이터가 수정된 상태
- Exclusive(배타) : 이 캐시에만 존재하고 메인 메모리 내용과 동일
- Shared(공유) : 둘 이상의 캐시에 데이터가 들어 있고 메인 메모리 내용과 동일한 상태
- Invalid(무효) : 다른 프로세스가 데이터를 수정하여 무효한 상태
- 캐시 라인(보통 64바이트) 상태를 다음 네 가지로 정의
- MESI 프로토콜
- 처음에는 매번 캐시 연산 결과를 바로 메모리에 기록 했다
write-through
(동시기록)- 메모리 대역폭을 많이 소모하는 등 효율이 낮아 현재는 거의 사용하지 않음
- 쓸 때는 무조건 메모리에 접근할테니, write는 메모리 쓰는거랑 똑같음
- 이게 싫어서 나온 게
write-back
(후기록)- write 시, cache 데이터만 변경함
- Dirty 데이터가 됨 즉, 캐시와 메인메모리 데이터가 다를 수 있다
- 때문에 수정 되었는지 확인하는 1cycle 이 필요함
- 최대 전송률을 결정하는 factor
- 메모리 클락율
- 메모리 버스 폭 (보통 64비트)
- 인터페이스 개수 (요즘은 대부분 2개)
- DDR RAM 은 최대 전송률이 2배
- DDR : Double Data Rate (이중 데이터 전송률)
- 클록 신호 양단에서 통신
- DDR : Double Data Rate (이중 데이터 전송률)
- 이 그래프를 못읽겠음
3.3 최신 프로세서의 특성
3.3.1 변환 색인 버퍼(TLB)
- 캐시에서 긴요하게 쓰이는 장치
- 가상 메모리 주소를 물리 메모리 주소로 매핑하는 페이지 테이블 캐시 역할 수행
- 가상 주소 참조해 물리 주소에 액세스하는 빈번한 작업 속도 향상
- JVM에도 TLB라는 메모리 관련 기능이 있는데, 약자만 같고 다른 말이라는데 찾아봐도 안나옴 JVM TLB는
3.3.2 분기 예측과 추측 실행
- 프로세서가 조건 분기하는 기준값을 평가하느라 대기하는 현상을 방지
- 조건문을 평가하기 까지 분기 이후 다음 명령을 알 수 없으므로, 프로세서는 여러 사이클 동안 멎게 된다
- 때문에 가장 발생 가능성이 큰 브랜치를 미리 결정하는 휴리스틱을 형성
- 미리 추측한 결과로 파이프라인을 채운다
- 예측이 맞으면 아무 일 없던 것 처럼 CPU 다음 작업 진행
- 틀리면 부분적으로 실행한 명령을 모두 폐기한 후 파이프라인을 비우는 대가를 치룸
3.3.3 하드웨어 메모리 모델
- 멀티코어 시스템에서 두 개 이상의 코어가 같은 메모리 주소에 접근한다면 같은 값을 볼 수 있을까?
- 이런 행위가 정확하게 동작하도록 메모리를 설계하는 것이 메모리 모델
- 강한 메모리 모델 : 언제나 모든 프로세서가 같은 주소에서 같은 값을 볼 수 있다
- 약한 메모리 모델 : 다른 프로세서의 write 연산을 현재 프로세서에 보여주거나, 현재 프로세서의 write 연산을 다른 프로세서에게 보여주기 위해 로컬 프로세서의 캐시를 무효화 하는 메모리 장벽이라는 특별한 명령어 집합을 가지고 있다고 한다 (lock, unlock 시 에 동작)
- 멀티스레드 코드가 제대로 작동하게 하려면 lock, volatile을 정확히 알아야 한다 (추후 학습)
3.4 운영체제
- OS의 주 임무는 여러 프로세스가 공유하는 리소스 액세스를 관장
- MMU(메모리 관리 유닛)을 통한 가상 주소 방식과 페이지 테이블은 메모리 액세스 제어의 핵심
- 한 프로세스가 소유한 메모리 영역을 다른 프로세스가 함부로 훼손하지 못하게 한다
3.4.1 스케줄러
- 프로레스 스케줄러 : CPU 액세스 통제
- 실행 큐 : 실행 대상이지만 CPU 차례를 기다려야 하는 스레드 혹은 프로세스의 대기장소
3.4.3 컨텍스트 교환
-
context switch : OS 스케줄러가 현재 실행 중인 스레드/태스크를 없애고 대기중인 다른 스레드/태스크로 대체하는 프로세스
-
유저 스레드가 preemption 도중 커널 모드로 바뀔 때 컨텍스트 스위치가 일어나고 비용이 크다
- 이를 최대한 만회하려고 리눅스는 vDSO 라는 장치를 제공한다
- 굳이 kernel privileges 가 필요 없는 시스템 콜 때 커널 모드로 컨텍스트 스위치를 하지 않겠다는 개념
- 자바에서도 이런 식으로 성능을 끌어 올릴 수 있다.
- 이를 최대한 만회하려고 리눅스는 vDSO 라는 장치를 제공한다
3.5 단순 시스템 모델
- 기본 컴포넌트
- 애플리케이션이 실행되는
- HW / OS
- JVM / 컨테이너
- 애플리케이션 코드
- 애플레케이션이 호출하는 외부 시스템
- 애플리케이션으로 유입되는 트래픽
- 애플리케이션이 실행되는
3.6 기본 감지 전략
-
애플리케이션이 잘 돌아간다는 건 CPU 사용량, 메모리, 네트워크, I/O 대역폭 등 시스템 리소스를 효율적으로 잘 이요하고 있다는 뜻
-
성능 진단의 첫 단추는 범인 찾기
- 어떤 리소스가 문제야?
3.6.1 CPU 사용률
- vmstat 1
- 1초마다 한 번 씩 찍어 다음 줄에 결과를 표시
- proc : 실행가능한(r) 프로세스, 블로킹된(b) 프로세스 개수
- memory : 스왑 메모리(swpd), 미사용 메모리(free), 버퍼로 사용되는 메모리(buff), 캐시로 사용한 메모리(cache)
- swap : 디스크로 교체되어 들어간 메모리(스왑인, si), 디스크에서 교체되어 빠져나온 메모리(스왑아웃, so)
- io : 블록인(bi), 블록아웃(bo)
- system : 인터럽트(in) 초당 context switch 횟수(cs)
- cpu : cpu 사용률(%), 유저시간(us), 커널시간(sy), 유휴시간(id), 대기시간(wa), 가상머신에 할애된 시간(st)
3.6.2 가비지 수집
- JVM 프로세스가 유저 공간에서 CPU를 100% 가깝게 사용하고 있다면 GC를 의심
3.6.3 입출력
- 파일 I/O 는 전체 시스템 성능에 암적인 존재
- 커널 바이패스 I/O 뭔지 모르겠음
3.6.4 기계공감
- 성능을 조금이라도 쥐어짜내야 하는 상황에서 하드웨어를 폭넓게 이해하고 공감할 수 있는 능력이 무엇보다 중요하다는 생각
3.7 가상화
- 이미 실행중인 OS 위에서 OS 사본을 하나의 프로세스로 실행시키는 모양
- 가상화 OS에서 실행하는 프로그램은 베어 메탈(즉, 비 가상화 OS)에서 실행될 때와 동일하게 작동해야 한다
- 하이퍼바이저는 모든 하드웨어 리소스 액세스를 조정해야한다
- 가상화 오버헤드는 가급적 작아야 하며 실행시간의 상당부를 차지해선 안된다
Chapter 6. 가비지 수집 기초
-
자바 가비지 수집의 요체는, 시스템에 있는 모든 객체의 수명을 정확히 몰라도 런타임이 대신 객체를 추적하며 쓸모없는 객체를 알아서 제거하는 것
- 가비지 수집 구현체의 기본 원칙 2가지
- 알고리즘은 반드시 모든 가비지를 수집해야 한다
- 살아 있는 객체는 절대로 수집해선 안된다
- 누가봐도 이게 엄청 중요
- 살아 있는 객체를 수집했다간 세그멘테이션 결함이 발생하거나, 프로그램 데이터가 조용히 더렵혀진다
6.1 마크 앤 스위프
- 초보적인 마크 앤 스위프 알고리즘
allocated list
(할당 리스트) : 메모리가 할당됐지만, 아직 회수되지 않은 객체- allocated list 순회, mark bit를 지운다
- GC 루트부터 살아 있는 객체를 찾는다
- 찾은 객체(살아있는 객체) 마다 mark bit 를 set 한다
- allocated list를 순회하면서 mark bit가 세팅되지 않은 객체를 찾는다
- 힙에서 메모리를 회수, free list 에 되돌린다
- allocated list 에서 삭제한다
- 살아있는 객체는 대부분
DFS
방식으로 찾는다- 이렇게 생성된 객체 그래프를
라이브 객체 그래프
or접근 가능한 객체의 전이 폐쇄
라고도 한다
- 이렇게 생성된 객체 그래프를
6.1.1 가비지 수집 용어
- STW
- GC Cycle 이 발생하여 가비지를 수집하는 동안에 모든 애플리케이션 스레드가 중단되는 현상
- Stop The World
- 동시
- GC 스레드와 어플리케이션 스레드가
동시(병행)
실행 될 수 있다는 개념
- GC 스레드와 어플리케이션 스레드가
- 보수
- 보수적인 스킴은 정확한 스킴의 정보가 없는 것
- 리소스를 낭비하는 일이 잦고, 근본적으로 타입 체계를 무시하기에 비효율적
- 압착
- 살아남은 객체들은 GC 사이클 마지막에 연속된 단일 영역으로 배열되며, 객체 쓰기가 가능한 여백의 시작점을 가리키는 포인터가 있다.
- 메모리 단편화를 방지
- 방출
- 수집 사이클 마지막에 할당된 영역을 완전히 비우고 살아남은 객체는 모두 다른 메모리 영역으로 이동(방출) 한다
6.2 핫스팟 런타임 개요
- 신기한게 java 는 진짜 call by reference 는 없나봄
- 전부 call by value 이고, 객체 레퍼런스의 경우 힙에 있는 주소
값
- 전부 call by value 이고, 객체 레퍼런스의 경우 힙에 있는 주소
6.2.1 객체를 런타임에 표현하는 방법
oop
: ordinary object pointer- 핫스팟은 런타임에 oop 라는 구조체로 자바 객체를 나타낸다
instanceOop
는 자바 클래스의 인스턴스를 나타냄- 인스턴스오오피의 메모리 레이아웃은 기계어 워드 2개로 구성된 헤더로 시작
Mark 워드
: 인스턴스 관련 메타데이터 가리키는 포인터Klass 워드
: 클래스 메타데이터 가리키는 포인터
- 인스턴스오오피의 메모리 레이아웃은 기계어 워드 2개로 구성된 헤더로 시작
- oop는 기계어 워드이기에 프로세서의 비트(32, 64) 를 따른다
- 메모리의 낭비를 줄이고자 핫스팟은
압축 oop
를 제공, 아래 oop 가 압축된다- 힙에 있는 모든 객체의 Klass 워드
- 참조형 인스턴스 필드
- 객체 배열의 각원소
- 옵션 : -XX:+UseCompressedOops (자바 7 이상, 64비트 힙은 디폴트 옵션임)
- 핫스팟 객체 헤더
- Mark 워드 (프로세서 비트(32, 64))
- Klass 워드 (압축 됐을 듯)
- 객체가 배열이면 length 워드 (항상 32비트)
- 32비트 여백 (정렬 규칙 때문에 필요할 경우)
- JVM 환경에서 자바 레퍼런스는 instanceOop(or null) 를 제외한 어떤 것도 가리킬 수 없다
- 자바 값은 기본 값 or instanceOop 레퍼런스
- 모든 자바 레퍼런스는 자바 힙의 주 영역에 있는 주소를 가리키는 포인터
- 자바 레퍼런스가 가리키는 주소에는 Mark 워드 + Klass 워드 가 들어있음
6.2.2 GC 루트 및 아레나
GC 루트
- 메모리 풀 외부에서 내부를 가리키는 포인터
- 메모리 풀 내부에서 내부의 다른 메모리 위치를 가리키는게 내부 포인터 인데 요놈은 정 반대인 외부 포인터 임
- 예) 스택에서 힙에 있는 객체를 가리킨다거나 이런 것. 힙 외부에서 힙 객체를 가리키는 거 아래에 많은 애들이 다 그렇다.
- 이 GC 루트들은 GC 루트 셋으로 관리되고, 이 GC 루트셋에서 GC 루트마다 DFS 로 가비지
- 종류
- 스택 프레임
- JNI
- 레지스터 (호이스트된 변수)
- JVM 코드 캐시에서 코드 루트
- 전역 객체
- load 된 클래스의 메타데이터
- 메모리 풀 외부에서 내부를 가리키는 포인터
아레나
- 핫스팟 GC가 동작하는 메모리 영역
- 핫스팟은 자바 heap을 관리할 때 시스템 콜을 하지 않는다
6.3 할당과 수명
- 가비지 수집의 주요 원인
할당률
- 일정 기간 새로 생성된 객체가 사용한 메모리 양
객체 수명
- 제대로 파악하기 어렵고, 더 핵심적 요인
- 가비지 수집은
메모리를 회수해 재사용
하는 일동일한 물리 메모리 조각을 몇 번이고 재사용 할 수 있는가
가 핵심이다- 객체가 생성된 후 잠시 존재하고
- 그 상태를 보관하는데 사용한 메모리를 다시 회수한다는 발상이 핵심!
6.3.1 약한 세대별 가설
- 가설 1 : 거의 대부분 객체는 아주 짧은 시간만 살아있고, 나머지 객체는 기대 수명이 훨씬 길다
- 단명 객체를 쉽고 빠르게 수집할 수 있는 설계, 장수 객체와 단명 객체를 완전히 떼어놓는게 좋다
- 객체마다 세대 카운트 (나이) 를 센다
- 큰 객체 외 에덴 공간에 생성한다
- 여기서 살아남은 객체는 다른 곳으로 옮긴다
- 장수 객체는 별도의 메모리 영역(올드 or 테뉴어드)에 보관
- 단명 객체를 쉽고 빠르게 수집할 수 있는 설계, 장수 객체와 단명 객체를 완전히 떼어놓는게 좋다
- 가설 2 : 늙은 객체가 젊은 객체를 참조할 일은 거의 없다
카드 테이블
이라는 자료구조에 늙은 객체가 젊은 객체를 참조하는 정보를 기록- 카드 테이블은 JVM이 관리하는 바이트 배열, 각 원소는 올드 세대 공간의 512 바이트 영역을 가리킨다
- 늙은 객체 o에 있는 참조형 필드값이 바뀌면 o에 해당하는 instanceOop가 들어 있는 카드를 찾아 해당 엔트리를 더티 마킹
- 핫스팟은 레퍼런스 필드를 업데이트 할 때 마다 write barrier를 이용한다.
- 아직 이해를 못했고 일단 적는다
- 카드 테이블은 JVM이 관리하는 바이트 배열, 각 원소는 올드 세대 공간의 512 바이트 영역을 가리킨다
- 객체 수명은
이원적 분포 양상
을 띈다 - 거의 대부분의 객체는 단명(아주 짧은 시간만 살아 있다)
- 나머지 객체는 기대 수명이 훨씬 길다
- 단명과 장수의 갭이 크다고 이해했음
- 대부분은 단명하나, 장수하는 객체는 진짜 장수한다
-
단순히 실험 및 경험에 의한 결론이다 과학적이나 이런건 없는 것 같음
- 약한 세대별 가설의 결론은
- 단명 객체를 쉽고 빠르게 수집할 수 있도록 설계할 것
- 장수 객체와 단명 객체를 완전히 떼어놓는 게 가장 좋다
- 핫스팟은 이러한 약한 세대별 가설을 십분 활용한다
- 대부분의 객체는
에덴
이라는 공간에 생성, 여기서 살아남은 객체는 다른 곳으로 옮긴다 - 객체마다
세대 카운트(나이)
를 센다 - 장수 객체는 별도의 메모리 영역(올드 세대)에 보관한다
- 대부분의 객체는
6.4 핫스팟의 가비지 수집
-
가비지 콜렉터가 객체를 이동시키는 것을
방출
이라고 한다 -
에덴은 대부분의 객체가 탄생하는 장소이고, 다음 GC 사이클까지도 못 버티는 수명이 짧은 객체는 다른 곳에 위치할 수 없으므로 특별히 관리를 잘해야 하는 영역이디
- JVM은 스레드별로 에덴을 여러 버퍼로 나눈다
- 이렇게 하면 다른 스레드가 내 버퍼에 객체를 할당하지 않음
- 이걸
스레드 로컬 할당 버퍼(TLAB)
이라고 함 - 동적으로 TLAB 크기를 조정한다
- 분명 활자를 다 이해했는데 이어지는 그림은 뭔지 알 길이 없음
카드테이블
이 뭔지도 모르겟다
- 방출 수집기
- 절반의 공간을 항상 완전히 비운다
- 살아있는 객체들을 다른 쪽으로 압착시켜 옮기고, 비워서 재사용
- 실제 보관 가능한 메모리 공간보다 2배를 더 사용하게 되는 낭비이긴 함
- 영 힙을
서바이버 공간
이라고 함 - 보통 서바이버 공간은 에덴보다 작다
6.5 병렬 수집기
- 병렬 수집기는 처리율에 최적화
- 영 GC, 풀 GC 모두 풀 STW를 일으킴
- 애플리케이션 스레드를 모두 중단 > 가용 CPU 코어를 총동원해 최대한 재빨리 메모리 수집
-
여러 스레드를 이용해 가급적 빠른 시간 내에 살아 있는 객체를 식별하고 기록작업을 최소화 하도록 설계
- 영 세대 병렬 수집
- 가장 흔한 가비지 수집 형태
- 스레드가 에덴에 객체를 할당하려하는데 자신이 할당받은 TLAB 공간이 부족한데, JVM이 새 TLAB을 할당할 수 없을 때
영 세대 수집
이 발생한다
영 세대 수집
이 일어나면 JVM은 STW- 어떤 스레드에서 객체를 할당할 수 없다면, 다른 스레드도 머지않아 똑같아 질테니까
- STW가 되면 핫스팟은 영 세대(에덴 및 비어있지 않은 서바이버 공간)을 뒤져서 가비지 아닌 객체(살아있는 객체)를 골라낸다
- 이 때 GC루트를 병렬 마킹 스캔 작업의 출발점으로 삼는다
- 살아남은 객체를 현재 비어있는 반대 서바이버 공간으로 방출한 후, 세대 카운트+1
- 마지막으로 에덴과 이제 막 객체들을 방출시킨 서바이버 공간을 재사용 가능하도록 빈 공간으로 만들고, 스레드 재시작 > TLAB을 애플리케이션 스레드에 배포
- 올드 세대 병렬 수집
- 올드 세대에 더 이상 방출할 공간이 없으면 병렬 수집기는 올드 세대 내부에서 객체들을 재배치, 늙은 객체가 죽고 빠져 버려진 공간을 회수
- 메모리 단편화가 일어날 일도 없고 아주 효율적이라는데 뭔소린지 잘 모르겠음
- 여튼 결론은 올드 세대 수집은 효율적이라는 것 같음
- 올드 공간은 영 세대 객체가 승격되거나, 올드/풀 수집이 일어나 객체를 재 탐색 후 재배치 하는 등의 수집이 일어날 때만 변한다
- 올드 세대에 더 이상 방출할 공간이 없으면 병렬 수집기는 올드 세대 내부에서 객체들을 재배치, 늙은 객체가 죽고 빠져 버려진 공간을 회수
- 병렬 수집기의 한계
- 풀 STW를 유발한다
- 올드 GC는 시간이 오래걸린다
6.6 할당의 역할
- GC는 어떤 고정된 일정에 맞춰 발생하는 것이 아니라, 필요에 의해 발생한다
- 불확정적, 불규칙
-
GC Cycle 은 하나 이상의 힙 메모리 공간이 꽉 채워져 더 이상 객체를 생성할 공간이 없을 때 발생한다
- 할당률이 너무 높아 객체가 어쩔수 없이 테뉴어드로 곧장 승격되는 것을
조기 승격
이라고 함
Subscribe via RSS