자바 최적화 (~ 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 문
  • 클래스 로딩
    • 부트스트랩 클래스가 자바 런타임 코어 클래스 로드
    • 확장 클래스 로더 생김, 부트스트랩 클래스로더가 자기 부모고, 필요할 때 클래스로딩 작업을 부모에게 넘긴다
    • 끝으로, 애플리케이션 클래스로더가 생성되고 지정된 클래스패스에 위치한 유저 클래스를 로드한다.
  • 자바는 프로그램 실행 중 처음보는 새 클래스를 디펜던시에 로드.
    • 찾지 못한 클래스로더는 자신의 부모 클래스로더에게 룩업을 넘김
    • 결국 부투스트랩도 룩업하지 못하면 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(무효) : 다른 프로세스가 데이터를 수정하여 무효한 상태
  • 처음에는 매번 캐시 연산 결과를 바로 메모리에 기록 했다
    • write-through (동시기록)
      • 메모리 대역폭을 많이 소모하는 등 효율이 낮아 현재는 거의 사용하지 않음
      • 쓸 때는 무조건 메모리에 접근할테니, write는 메모리 쓰는거랑 똑같음
  • 이게 싫어서 나온 게
    • write-back (후기록)
      • write 시, cache 데이터만 변경함
      • Dirty 데이터가 됨 즉, 캐시와 메인메모리 데이터가 다를 수 있다
      • 때문에 수정 되었는지 확인하는 1cycle 이 필요함
  • 최대 전송률을 결정하는 factor
    • 메모리 클락율
    • 메모리 버스 폭 (보통 64비트)
    • 인터페이스 개수 (요즘은 대부분 2개)
  • DDR RAM 은 최대 전송률이 2배
    • 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 가 필요 없는 시스템 콜 때 커널 모드로 컨텍스트 스위치를 하지 않겠다는 개념
    • 자바에서도 이런 식으로 성능을 끌어 올릴 수 있다.

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 (할당 리스트) : 메모리가 할당됐지만, 아직 회수되지 않은 객체
      1. allocated list 순회, mark bit를 지운다
      2. GC 루트부터 살아 있는 객체를 찾는다
      3. 찾은 객체(살아있는 객체) 마다 mark bit 를 set 한다
      4. allocated list를 순회하면서 mark bit가 세팅되지 않은 객체를 찾는다
      • 힙에서 메모리를 회수, free list 에 되돌린다
      • allocated list 에서 삭제한다
    • 살아있는 객체는 대부분 DFS 방식으로 찾는다
      • 이렇게 생성된 객체 그래프를 라이브 객체 그래프 or 접근 가능한 객체의 전이 폐쇄 라고도 한다
6.1.1 가비지 수집 용어
  • STW
    • GC Cycle 이 발생하여 가비지를 수집하는 동안에 모든 애플리케이션 스레드가 중단되는 현상
    • Stop The World
  • 동시
    • GC 스레드와 어플리케이션 스레드가 동시(병행) 실행 될 수 있다는 개념
  • 보수
    • 보수적인 스킴은 정확한 스킴의 정보가 없는 것
    • 리소스를 낭비하는 일이 잦고, 근본적으로 타입 체계를 무시하기에 비효율적
  • 압착
    • 살아남은 객체들은 GC 사이클 마지막에 연속된 단일 영역으로 배열되며, 객체 쓰기가 가능한 여백의 시작점을 가리키는 포인터가 있다.
    • 메모리 단편화를 방지
  • 방출
    • 수집 사이클 마지막에 할당된 영역을 완전히 비우고 살아남은 객체는 모두 다른 메모리 영역으로 이동(방출) 한다

6.2 핫스팟 런타임 개요

  • 신기한게 java 는 진짜 call by reference 는 없나봄
    • 전부 call by value 이고, 객체 레퍼런스의 경우 힙에 있는 주소
6.2.1 객체를 런타임에 표현하는 방법
  • oop : ordinary object pointer
    • 핫스팟은 런타임에 oop 라는 구조체로 자바 객체를 나타낸다
    • instanceOop 는 자바 클래스의 인스턴스를 나타냄
      • 인스턴스오오피의 메모리 레이아웃은 기계어 워드 2개로 구성된 헤더로 시작
        • Mark 워드 : 인스턴스 관련 메타데이터 가리키는 포인터
        • Klass 워드 : 클래스 메타데이터 가리키는 포인터
    • 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를 이용한다.
          • 아직 이해를 못했고 일단 적는다
  • 객체 수명은 이원적 분포 양상을 띈다
  • 거의 대부분의 객체는 단명(아주 짧은 시간만 살아 있다)
  • 나머지 객체는 기대 수명이 훨씬 길다
    • 단명과 장수의 갭이 크다고 이해했음
    • 대부분은 단명하나, 장수하는 객체는 진짜 장수한다
  • 단순히 실험 및 경험에 의한 결론이다 과학적이나 이런건 없는 것 같음

  • 약한 세대별 가설의 결론은
    1. 단명 객체를 쉽고 빠르게 수집할 수 있도록 설계할 것
    2. 장수 객체와 단명 객체를 완전히 떼어놓는 게 가장 좋다
  • 핫스팟은 이러한 약한 세대별 가설을 십분 활용한다
    • 대부분의 객체는 에덴 이라는 공간에 생성, 여기서 살아남은 객체는 다른 곳으로 옮긴다
    • 객체마다 세대 카운트(나이) 를 센다
    • 장수 객체는 별도의 메모리 영역(올드 세대)에 보관한다

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 은 하나 이상의 힙 메모리 공간이 꽉 채워져 더 이상 객체를 생성할 공간이 없을 때 발생한다

  • 할당률이 너무 높아 객체가 어쩔수 없이 테뉴어드로 곧장 승격되는 것을 조기 승격 이라고 함