• 자바 세계에서는 거의 모든 객체 인스턴스가 힙에 저장
  • 가비지 컬렉터가 힙 청소하려면 어떤 객체가 살아 있고, 죽었는지 판단해야됨

3.2.1 참조 카운팅 알고리즘

  • 많은 책에서 객체의 살아있음과 죽음 판단 알고리즘 다음과 같이 설명함
      1. 객체를 가리키는 참조 카운터를 추가. 참조하는 곳이 하나 늘어날 때마다 카운터 값 1씩 증가
      1. 참조하는 곳이 하나 사라질 때마다 카운터 값 1씩 감소
      1. 카운터 값이 0이된 객체는 더는 사용될 수 없음
      • 이건 횟수 세기위해 약간의 메모리 추가 사용하지만 원리 간단하고 판단이 쉬움. 파이썬, 러스트 등에 쓰임
  • 하지만, 자바에서는 이거 안쓴다.
    • 간결한 알고리즘에도 모든 상황에서 잘 동자갛게 하려면 계산할게 늘어남
    • 예를 들어, 참조 카운팅만으로는 순환 참조 문제 풀기 어려움
    • 외부 참조 있는 두 객체를 서로를 참조하게하는 변수두고 외부 참조를 없엠. 그리고나면 외부에서 접근을 못하는데 이건 서로 참조하고 있으니 카운터가 0이 아니라서 회수가 불가
      • 실제 자바에서 위 실험하면 회수가 됨. 즉 자바 가상 머신은 참조 카운팅 안씀

3.2.2 도달 가능성 분석 알고리즘

  • Java, C# 등 주류부터 리스프까지 모두 오늘날에는 객체 생사 판단에 ‘도달 가능성 분석’ 알고리즘 이용
    • 이 알고리즘 기본 아이디어는 GC 루트라고 하는 루트 객체들을 시작 노드 집합으로 쓰는 것
    • 시작 노드들에서 출발하여 참조하는 다른객체들로 탐색해 들어감
    • 탐색 과정에서 만들어지는 경로를 참조 체인(reference chain)이라 함.
    • 어떤 객체와 GC 루트 사이를 이어주는 참조 체인이 없다면, 즉 GC 루트로 부터 도달 불가능한 객체는 더 이상 사용할 수 없는게 확실해짐!
      • 회수 대상이 됨
  • 자바에서 GC 루트로 이용할 수 있는 객체는 정해져있고 다음은 그 예
    • 가상 머신 스택(스택 프레임의 지역 변수 테이블)에서 참조하는 객체: 현재 실행중인 메서드에서 쓰는 매개 변수, 지역 변수, 임시 변수 등
    • 메서드 영역에서 클래스가 정적 필드로 참조하는 객체: 자바 클래스의 참조 타입 정적 변수
    • 메서드 영역에서 상수로 참조되는 객체: 문자열 테이블 안의 객체
    • 네이티브 메서드 스택에서 JNI(이른바 네이티브 메서드)가 참조하는 객체
    • 자바 가상 머신 내부에서 쓰이는 참조: 기본 데이터 타입에 해당하는 Class 객체, (NPE, OOM 등) 이루 상주 예외 객체, 시스템 클래스 로더
    • 동리화 락(synchroized 키워드)로 잠겨있는 모든 객체
    • 자바 가상 머신 내부 상황 반영하는 JMXBean: JVMTI 에 등록된 콜백, 로컬 코드 캐시 등
      • 이상의 정해진 GC 루트 외에도 GC종류나 현재 회수중인 메모리 영역에따라 다른 객체도 임시로 추가될 수 있음
  • 부분 컬렉션 이라는 개념 존재. 자바 힙 일부부터 분석 시작
  • 메모리 영역이 서로 완전히 격리되거다 닫혀있는것 유의

3.2.3 다시 참조 이야기로

  • JDK 1.2 전의 참조 정의
    • 참조 차입 데이터에 저장된 값이 다른 메모리 조각의 시작 주소를 뜻한다면, 이 참조 데이터를 해당 메모리 조각이나 객체를 참조한다고 말한다
  • 현 시점에서는 좁은 정의.
  • 참조됐다, 참조되지 않았다 2개 뿐. 버리기에는 아까운 객체를 표현할 방법 없음
  • GC 하고나서도 메모리가 부족할때 그때 회수하는 객체를 표현하려면?
    • 캐시 기능에대한 시나리오
  • JDK 1.2 부터 참조 개념 확장되어 네 가지로 구분
    • 강한 참조
      • 전통적인 정의의 참조.
      • Object obj = new Object() 처럼 코드에서 참조를 할당하는 것 말함
      • 강한 참조 관계 남아있는 객체는 가비지 컬렉터가 절대 회수하지 않음
    • 부드러운 참조
      • 유용하지만 필수는 아닌 객체
      • 부드러운 참조만 남은 객체라면 메모리 오버플로 나기 직전에 두번째 회수를 위한 회수 목록에 추가됨
      • 두 번째 회수 후에도 메모리 부족시 그때 메모리 오버플로 예외 던짐
      • SoftReference 클래스 형태
    • 약한 참조
      • 부드러운 참조와 비슷하지만 연결 강도 더 약함
      • 약한 참조뿐인 객체는 다음번 가비지 컬랙션까지만 살아 있음.
      • GC 동작하기 시작하면 메모리 넉넉해도 회수됨
      • WeakReference
    • 유령 참조
      • 참조 중 가장 약함
      • 객체 수명에 아무런 영향 없다
      • 유령 참조 통해 객체 인스턴스 가져오는 것마저 불가
      • 유령 참조 거는 유일한 목적은 대상 객체가 회수 될 때 알림을 받기 위함
      • 마찬가지로 1.2때 추가되었고 PhantomReference
    • +알파 파이널 참조
      • JDK 내부적으로는 파이널 참조라는 유형도 쓰임
      • 약한 참조와 유령 참조 사이
      • finalize() 메서드를 구현한 객체는 모두 파이널 참조 대상되어 별도의 큐에 등록됨
      • 그런 다음 해댱 객체에 도달할 수 있는 강한 참조, 부드러운 참조, 약한 참조가 모두 없어지면 finalize()메서드 호출함!

3.2.4 살았나 죽었나?

  • 도달 가능성 알고리즘이 ‘도달 불가능’으로 판단한 객체라고 해서 반드시 죽어야 하는 건 아님.
    • 아직 ‘유예’ 단계가 남음
    • 확실한 사망 선고 내리려면 두번의 표시(marking) 과정 거쳐야 함
  • 도달 가능성 분석으로 GC 루트와 연결된 참조 체인 못찾은 객체는 첫 번째 표시가 이루어 지며 이어서 필터링 진행
    • 필터링 조건은 종료자(finalizer), 즉 finalize() 메서드를 싱행해야 하는 객체인가다. finalize()가 필요 없는 객체거나 가상머신이 이미 finalize() 호출한 경우 모두 ‘실행할 필요 없음’으로 처리
  • finalize() 실행해야하는 객체로 판명되면 F-Queue 라는 대기열에 추가됨.
    • 그러면 가상 머신이 나중에 우선순위 낮은 종료자 스레드 생성해 F-Queue에 있는 객체들의 finalize()를 실행!
      • 참고로 가상 머신은 미 메서드 실행만 시키고 기다리지 않음.
      • finalize()가 너무 오래 걸리거나 무한루프에 빠질때 문제가 되기 때문
      • 최아의 경우 GC 시스템 전체를 비정상 종료 시킬수도…
  • finalize() 메서드는 죽음에 직면한 객체가 부활할 수 있는 마지막 기회
    • 부활하려면 참조 체인상의 아무 객체와 다시 연결됨면 됨
    • 예를 들어 자기자신(this)를 특정 클래스 변수나 다른 객체의 인스턴스 변수에 할당
      • 이렇게하면 두번째 표시 과정에서 ‘회수 대상’목록에서 제외
  • 100 페이지 코드 참고
  • 예제코드를 보면 어떤 객체든 시스템이 finalize()를 호출해 주는 건 오직 1번임이 확인 가능!
  • finalize() 사용을 지양. 왜?
    • 이건 C,C++ 파괴자 와 다름
    • 종료자 가능은 자바로 C,C++개발자들 끌어들이려 도입한 타협안
    • 실행 비용 높고, 불확실성 큼
    • finalize()로 할 수 있는 일은 전부 try-finally 등 으로 더 잘 처리 가능

3.2.5 메서드 영역 회수하기

  • (핫스팟 가상 머신의 메타스페이스 or 영구 세대) 메서드 영역은 가비지 컬렉션 대상 아니라고 생각하는 사람 있음(아니 아니라메;;)
    • <명세>따르면 가컬이 메서드 영역 반드시 청소 해야되는 건 아님
    • 메서드 영역 가컬은 가성비가 안나와서
    • 일반적인 앱에서 자바 힙은, 그중에서도 특히 신세대는 가비지 컬렉션 한 번으로 메모리 공간의 70~99%를 회수해 냄
    • 반면 메서드 영역은 회수 조건 까다로워 효율이 떨어짐
  • 메서드 영역 가컬은 크게 두 가지를 회수
    • 더 이상 사용안되는 ‘상수’, ‘클래스’
  • 다 쓴 ‘상수’ 회수 법도 자바 힙에서 객체를 회수하는 방법과 유사
    • 상수 풀에서 리터럴을 회수하는 예를 보여줌
      • 문자열 “자바”가 상수풀에 있음
      • 현재 시스템에서 “자바”인 문자열 객체는 하나도 없다고 가정해보자
      • 즉, 상수 풀 안의 “자바” 상수를 참조하는 문자열 객체 전혀 없고, 가상 머신에서 이 리터럴을 사용하는 코드가 한 곳도 없는 것
      • 이 시점에서 회수 시작되면 가컬은 “자바”상수를 상수 풀에서 치워야한다고 판단할 것
      • 상수 풀에 있는 다른 클래스(인터페이스 포함), 메서드, 필드의 심벌 참조도 비슷한 방법으로 회수함
    • 다 쓴 상수인지 판단은 쉬운편 하지만! 더 이상 쓰이지 않는 ‘클래스’판단 조건은 더 까다로움. 다음 3가지 조건 만족해야됨
      • 이 클래스의 인스턴스가 모두 회수됨. 즉, 자바 힙에는 해당 클래스와 하위 클래스의 인스턴스가 하나도 존재 않음
      • 이 클래스를 읽어 들인 클래스 로더가 회수됨. 이 조건은 OSGi or JSP 리로딩처럼 세심하게 설계된 대안 클래스 로더 없이는 충족 어려움
      • 이 클래스에 해당하는 java.lang.Class 객체를 아무 곳에서도 참조 않고, 리플렉션 기능으로 이 클래스의 메서드를 이요하는 곳도 전혀 없다
    • 자바 가상 머신의 위 3조건 부합하는 쓸모없는 클래스 회수하도록 ‘허용’함.
      • 여기서 허용이지 반드시 회수한다고는 안함!
    • 핫스팟은 클래스 회수 여부 제어할 수 있도록 -Xnoclassgc 제공
      • 또한 -verbose:class, -Xlog:class+load=info, -Xlog:class+unload=info 로 클래스가 로딩되고 언로딩되는 정보 볼 수 있음
    • 리플렉션, 동적 프락시, CGLib 같은 바이트코드 프레임워크를 많이 쓰는 경우나 JSP를 동적으로 생성하고 클래스 로더 자주 사용자화하는 OSGi 환경 등에서는 일반적으로 자바 가상 머신이 타입 언로딩을 지원해야함…
      • 그래야 메서드 영역이 과도한 압박에 시달리는 일 방지 가능하기 때문