Java Virtual Machine-빠른 가이드

JVM은 사양이며 사양을 준수하는 한 다른 구현을 가질 수 있습니다. 사양은 아래 링크에서 찾을 수 있습니다-https://docs.oracle.com

Oracle에는 자체 JVM 구현 (HotSpot JVM이라고 함)이 있고 IBM에는 자체 JVM (예 : J9 JVM)이 있습니다.

사양 내에 정의 된 작업은 아래에 나와 있습니다 (출처-Oracle JVM 사양, 위 링크 참조)-

  • '클래스'파일 형식
  • 데이터 유형
  • 기본 유형 및 값
  • 참조 유형 및 값
  • 런타임 데이터 영역
  • Frames
  • 물체의 표현
  • 부동 소수점 산술
  • 특별한 방법
  • Exceptions
  • 명령어 세트 요약
  • 클래스 라이브러리
  • 공공 디자인, 개인 구현

JVM은 가상 머신, 자체 ISA, 자체 메모리, 스택, 힙 등이있는 추상 컴퓨터입니다. 호스트 OS에서 실행되며 여기에 리소스에 대한 수요를 배치합니다.

HotSpot JVM 3의 아키텍처는 다음과 같습니다.

실행 엔진은 가비지 수집기와 JIT 컴파일러로 구성됩니다. JVM은 두 가지 특징이 있습니다.client and server. 둘 다 동일한 런타임 코드를 공유하지만 사용되는 JIT가 다릅니다. 이에 대해서는 나중에 자세히 알아볼 것입니다. 사용자는 JVM 플래그 -client 또는 -server를 지정하여 사용할 플레이버를 제어 할 수 있습니다 . 서버 JVM은 서버에서 장기 실행되는 Java 애플리케이션을 위해 설계되었습니다.

JVM은 32b 및 64b 버전으로 제공됩니다. 사용자는 VM 인수에 -d32 또는 -d64를 사용하여 사용할 버전을 지정할 수 있습니다. 32b 버전은 최대 4G의 메모리 만 처리 할 수 ​​있습니다. 메모리에 대용량 데이터 세트를 유지하는 중요한 애플리케이션을 통해 64b 버전은 이러한 요구를 충족합니다.

JVM은 클래스 및 인터페이스를 동적으로로드, 링크 및 초기화하는 프로세스를 관리합니다. 로딩 과정에서JVM finds the binary representation of a class and creates it.

연결 과정에서 loaded classes are combined into the run-time state of the JVM so that they can be executed during the initialization phase. JVM은 기본적으로 연결 프로세스를 위해 런타임 상수 풀에 저장된 기호 테이블을 사용합니다. 초기화는 실제로executing the linked classes.

로더 유형

그만큼 BootStrap클래스 로더는 클래스 로더 계층 구조의 맨 위에 있습니다. JRE의 lib 디렉토리 에있는 표준 JDK 클래스를로드합니다 .

그만큼 Extension 클래스 로더는 클래스 로더 계층의 중간에 있으며 부트 스트랩 클래스 로더의 직계 자식이며 JRE의 lib \ ext 디렉토리에있는 클래스를로드합니다.

그만큼 Application클래스 로더는 클래스 로더 계층 구조의 맨 아래에 있으며 애플리케이션 클래스 로더의 직계 하위입니다. 에 의해 지정된 jar 및 클래스를로드합니다.CLASSPATH ENV 변하기 쉬운.

연결

연결 프로세스는 다음 세 단계로 구성됩니다.

Verification− 이는 생성 된 .class 파일 (바이트 코드)이 유효한지 확인하기 위해 Bytecode Verifier에 의해 수행됩니다. 그렇지 않으면 오류가 발생하고 연결 프로세스가 중단됩니다.

Preparation − 메모리는 클래스의 모든 정적 변수에 할당되며 기본값으로 초기화됩니다.

Resolution− 모든 기호 메모리 참조는 원래 참조로 대체됩니다. 이를 위해 클래스 메서드 영역의 런타임 상수 메모리에있는 기호 테이블이 사용됩니다.

초기화

이것은 클래스 로딩 프로세스의 마지막 단계입니다. 정적 변수에는 원래 값이 할당되고 정적 블록이 실행됩니다.

JVM 사양은 프로그램 실행 중에 필요한 특정 런타임 데이터 영역을 정의합니다. 그들 중 일부는 JVM이 시작되는 동안 생성됩니다. 다른 것들은 스레드에 로컬이며 스레드가 생성 될 때만 생성됩니다 (그리고 스레드가 파괴 될 때 파괴됩니다). 이들은 아래에 나열되어 있습니다-

PC (프로그램 카운터) 등록

각 스레드에 로컬이며 스레드가 현재 실행중인 JVM 명령어의 주소를 포함합니다.

스택

각 스레드에 대해 로컬이며 메서드 호출 중에 매개 변수, 로컬 변수 및 반환 주소를 저장합니다. 스레드가 허용 된 것보다 더 많은 스택 공간을 요구하면 StackOverflow 오류가 발생할 수 있습니다. 스택이 동적으로 확장 가능한 경우에도 OutOfMemoryError가 발생할 수 있습니다.

더미

모든 스레드간에 공유되며 런타임 중에 생성되는 객체, 클래스의 메타 데이터, 배열 등이 포함됩니다. JVM이 시작될 때 생성되고 JVM이 종료 될 때 파괴됩니다. 특정 플래그를 사용하여 OS에서 JVM이 요구하는 힙의 양을 제어 할 수 있습니다 (나중에 자세히 설명). 성능에 중요한 영향을 미치므로 메모리를 너무 적게 또는 너무 많이 요구하지 않도록주의해야합니다. 또한 GC는이 공간을 관리하고 계속해서 죽은 개체를 제거하여 공간을 확보합니다.

방법 영역

이 런타임 영역은 모든 스레드에 공통이며 JVM이 시작될 때 작성됩니다. 상수 풀 (나중에 자세히 설명), 생성자 및 메서드에 대한 코드, 메서드 데이터 등과 같은 클래스 별 구조를 저장합니다. JLS는이 영역이 가비지 수집되어야하는지 여부를 지정하지 않으므로 JVM은 GC를 무시하도록 선택할 수 있습니다. 또한 이것은 응용 프로그램의 요구에 따라 확장되거나 확장되지 않을 수 있습니다. JLS는 이와 관련하여 어떤 것도 요구하지 않습니다.

런타임 상수 풀

JVM은로드 된 클래스를 링크하는 동안 심볼 테이블 (여러 역할 중 하나) 역할을하는 클래스 별 / 유형별 데이터 구조를 유지합니다.

네이티브 메서드 스택

스레드가 원시 메소드를 호출하면 Java 가상 머신의 구조 및 보안 제한이 더 이상 자유를 방해하지 않는 새로운 세계로 들어갑니다. 네이티브 메서드는 가상 머신의 런타임 데이터 영역에 액세스 할 수 있지만 (네이티브 메서드 인터페이스에 따라 다름) 원하는 다른 작업도 수행 할 수 있습니다.

가비지 컬렉션

JVM은 Java에서 개체의 전체 수명주기를 관리합니다. 개체가 생성되면 개발자는 더 이상 걱정할 필요가 없습니다. 객체가 죽은 경우 (즉, 더 이상 참조가 없음) 직렬 GC, CMS, G1 등 여러 알고리즘 중 하나를 사용하여 GC가 힙에서 객체를 배출합니다.

GC 프로세스 동안 개체는 메모리에서 이동됩니다. 따라서 이러한 개체는 프로세스가 진행되는 동안 사용할 수 없습니다. 프로세스가 진행되는 동안 전체 응용 프로그램을 중지해야합니다. 이러한 일시 중지는 '세계 중지'일시 중지라고하며 엄청난 오버 헤드입니다. GC 알고리즘은 주로이 시간을 줄이는 것을 목표로합니다. 이에 대해서는 다음 장에서 자세히 설명하겠습니다.

GC 덕분에 Java에서 메모리 누수는 매우 드물지만 발생할 수 있습니다. Java에서 메모리 누수를 생성하는 방법은 이후 장에서 살펴볼 것입니다.

이 장에서는 JIT 컴파일러와 컴파일 된 언어와 해석 된 언어의 차이점에 대해 알아 봅니다.

컴파일 된 언어와 해석 된 언어

C, C ++ 및 FORTRAN과 같은 언어는 컴파일 된 언어입니다. 그들의 코드는 기본 컴퓨터를 대상으로하는 바이너리 코드로 제공됩니다. 즉, 기본 아키텍처를 위해 특별히 작성된 정적 컴파일러에 의해 한 번에 상위 수준 코드가 이진 코드로 컴파일됩니다. 생성 된 바이너리는 다른 아키텍처에서 실행되지 않습니다.

반면에 Python 및 Perl과 같은 번역 언어는 유효한 인터프리터가있는 한 모든 컴퓨터에서 실행할 수 있습니다. 상위 수준 코드를 한 줄씩 살펴보고 이진 코드로 변환합니다.

해석 된 코드는 일반적으로 컴파일 된 코드보다 느립니다. 예를 들어 루프를 고려하십시오. 인터 프리트는 루프가 반복 될 때마다 해당 코드를 변환합니다. 반면에 컴파일 된 코드는 번역을 하나만 만듭니다. 또한 인터프리터는 한 번에 한 줄만 볼 수 있기 때문에 컴파일러와 같은 명령문 실행 순서 변경과 같은 중요한 코드를 수행 할 수 없습니다.

아래에서 이러한 최적화의 예를 살펴 보겠습니다.

Adding two numbers stored in memory. 메모리 액세스는 여러 CPU 사이클을 소비 할 수 있으므로 좋은 컴파일러는 메모리에서 데이터를 가져 와서 데이터를 사용할 수있을 때만 추가를 실행하라는 명령을 내립니다. 기다리지 않고 그 동안 다른 명령을 실행합니다. 반면에 인터프리터가 주어진 시간에 전체 코드를 인식하지 못하기 때문에 해석 중에 그러한 최적화는 불가능합니다.

그러나 통역 언어는 해당 언어의 유효한 통역사가있는 모든 컴퓨터에서 실행될 수 있습니다.

Java가 컴파일되거나 해석됩니까?

자바는 중간 지점을 찾으려고했습니다. JVM은 javac 컴파일러와 기본 하드웨어 사이에 있기 때문에 javac (또는 다른 컴파일러) 컴파일러는 플랫폼 별 JVM에서 이해하는 바이트 코드에서 Java 코드를 컴파일합니다. 그런 다음 JVM은 코드가 실행될 때 JIT (Just-in-time) 컴파일을 사용하여 바이너리로 Bytecode를 컴파일합니다.

핫스팟

일반적인 프로그램에는 자주 실행되는 코드의 작은 부분 만 있으며, 전체 애플리케이션의 성능에 큰 영향을 미치는 것은이 코드입니다. 이러한 코드 섹션을HotSpots.

코드의 일부 섹션이 한 번만 실행되면 컴파일하는 것은 노력 낭비이며 대신 바이트 코드를 해석하는 것이 더 빠릅니다. 그러나 섹션이 핫 섹션이고 여러 번 실행되면 JVM이 대신 컴파일합니다. 예를 들어 메서드가 여러 번 호출되는 경우 코드를 컴파일하는 데 걸리는 추가주기는 생성되는 더 빠른 바이너리에 의해 오프셋됩니다.

또한 JVM이 특정 메소드 또는 루프를 더 많이 실행할수록 더 많은 정보를 수집하여 더 빠른 바이너리가 생성되도록 잡다한 최적화를 수행합니다.

다음 코드를 살펴 보겠습니다.

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

이 코드가 해석되면 인터프리터는 obj1의 클래스를 반복 할 때마다 추론합니다. 이는 Java의 각 클래스에 .equals () 메서드가 있기 때문입니다. 이는 Object 클래스에서 확장되고 재정의 될 수 있습니다. 따라서 obj1이 각 반복의 문자열 인 경우에도 추론은 계속 수행됩니다.

반면에 실제로 일어날 일은 JVM이 각 반복에 대해 obj1이 String 클래스이므로 String 클래스의 .equals () 메서드에 해당하는 코드를 직접 생성한다는 것을 JVM이 알아 차리는 것입니다. 따라서 조회가 필요하지 않으며 컴파일 된 코드가 더 빨리 실행됩니다.

이러한 종류의 동작은 JVM이 코드 동작 방식을 알고있는 경우에만 가능합니다. 따라서 코드의 특정 섹션을 컴파일하기 전에 대기합니다.

아래는 또 다른 예입니다.

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

인터프리터는 각 루프에 대해 메모리에서 'sum'의 값을 가져 와서 'I'를 추가 한 다음 다시 메모리에 저장합니다. 메모리 액세스는 비용이 많이 드는 작업이며 일반적으로 여러 CPU주기가 필요합니다. 이 코드는 여러 번 실행되므로 HotSpot입니다. JIT는이 코드를 컴파일하고 다음과 같이 최적화합니다.

'sum'의 로컬 사본은 특정 스레드에 특정한 레지스터에 저장됩니다. 모든 작업은 레지스터의 값으로 수행되고 루프가 완료되면 값이 메모리에 다시 기록됩니다.

다른 스레드도 변수에 액세스하면 어떻게됩니까? 다른 스레드가 변수의 로컬 복사본을 업데이트하기 때문에 오래된 값을 보게됩니다. 이러한 경우 스레드 동기화가 필요합니다. 매우 기본적인 동기화 프리미티브는 'sum'을 휘발성으로 선언하는 것입니다. 이제 변수에 액세스하기 전에 스레드는 로컬 레지스터를 플러시하고 메모리에서 값을 가져옵니다. 액세스 후 값은 즉시 메모리에 기록됩니다.

다음은 JIT 컴파일러가 수행하는 몇 가지 일반적인 최적화입니다.

  • 메서드 인라인
  • 데드 코드 제거
  • 호출 사이트 최적화를위한 휴리스틱
  • 일정한 접기

JVM은 5 가지 컴파일 레벨을 지원합니다-

  • Interpreter
  • 전체 최적화가 적용된 C1 (프로파일 링 없음)
  • 호출 및 백 에지 카운터가있는 C1 (라이트 프로파일 링)
  • 전체 프로파일 링이있는 C1
  • C2 (이전 단계의 프로파일 링 데이터 사용)

모든 JIT 컴파일러를 비활성화하고 인터프리터 만 사용하려면 -Xint를 사용하십시오.

클라이언트 대 서버 JIT

-client 및 -server를 사용하여 각 모드를 활성화합니다.

클라이언트 컴파일러 (C1)는 서버 컴파일러 (C2)보다 빨리 코드 컴파일을 시작합니다. 따라서 C2가 컴파일을 시작했을 때 C1은 이미 코드 섹션을 컴파일했을 것입니다.

그러나 기다리는 동안 C2는 C1보다 더 많이 알기 위해 코드를 프로파일 링합니다. 따라서 최적화에 의한 오프셋을 사용하여 훨씬 빠른 바이너리를 생성 할 수있는 경우 대기하는 시간입니다. 사용자의 관점에서 볼 때 프로그램 시작 시간과 프로그램 실행에 걸리는 시간 사이의 절충안이 있습니다. 시작 시간이 프리미엄이면 C1을 사용해야합니다. 애플리케이션이 오랫동안 실행될 것으로 예상되는 경우 (일반적으로 서버에 배포 된 애플리케이션) C2를 사용하는 것이 더 빠른 코드를 생성하여 추가 시작 시간을 크게 상쇄하므로 더 좋습니다.

IDE (NetBeans, Eclipse) 및 기타 GUI 프로그램과 같은 프로그램의 경우 시작 시간이 중요합니다. NetBeans를 시작하는 데 1 분 이상 걸릴 수 있습니다. NetBeans와 같은 프로그램이 시작되면 수백 개의 클래스가 컴파일됩니다. 이러한 경우 C1 컴파일러가 최선의 선택입니다.

C1에는 두 가지 버전이 있습니다. 32b and 64b. C2는64b.

계층 형 컴파일

Java의 이전 버전에서 사용자는 다음 옵션 중 하나를 선택할 수 있습니다.

  • 통역사 (-Xint)
  • C1 (-클라이언트)
  • C2 (-서버)

Java 7에서 제공되었습니다. C1 컴파일러를 사용하여 시작하고 코드가 뜨거워지면 C2로 전환합니다. 다음 JVM 옵션으로 활성화 할 수 있습니다. -XX : + TieredCompilation. 기본값은set to false in Java 7, and to true in Java 8.

5 가지 컴파일 계층 중 계층 형 컴파일은 1 -> 4 -> 5.

32b 시스템에서는 32b 버전의 JVM 만 설치할 수 있습니다. 64b 시스템에서 사용자는 32b 버전과 64b 버전 중에서 선택할 수 있습니다. 그러나 Java 애플리케이션의 성능에 영향을 미칠 수있는 특정 뉘앙스가 있습니다.

Java 애플리케이션이 4G 미만의 메모리를 사용하는 경우 64b 시스템에서도 32b JVM을 사용해야합니다. 이는이 경우 메모리 참조가 32b 일 뿐이고이를 조작하는 것이 64b 주소를 조작하는 것보다 비용이 적게 들기 때문입니다. 이 경우 64b JVM은 OOPS (일반 개체 포인터)를 사용하더라도 성능이 저하됩니다. OOPS를 사용하면 JVM은 64b JVM에서 32b 주소를 사용할 수 있습니다. 그러나 기본 기본 참조가 여전히 64b이므로 조작하는 것이 실제 32b 참조보다 느립니다.

애플리케이션이 4G 이상의 메모리를 소비 할 경우 32b 참조가 4G 이상의 메모리를 처리 할 수 ​​없기 때문에 64b 버전을 사용해야합니다. 동일한 시스템에 두 버전을 모두 설치할 수 있으며 PATH 변수를 사용하여 두 버전간에 전환 할 수 있습니다.

이 장에서는 JIT 최적화에 대해 알아 봅니다.

메서드 인라이닝

이 최적화 기술에서 컴파일러는 함수 호출을 함수 본문으로 대체하기로 결정합니다. 아래는 동일한 예입니다.

int sum3;

static int add(int a, int b) {
   return a + b;
}

public static void main(String…args) {
   sum3 = add(5,7) + add(4,2);
}

//after method inlining
public static void main(String…args) {
   sum3 = 5+ 7 + 4 + 2;
}

이 기술을 사용하여 컴파일러는 함수 호출을 수행하는 오버 헤드로부터 머신을 저장합니다 (파라미터를 스택에 푸시하고 팝해야 함). 따라서 생성 된 코드가 더 빠르게 실행됩니다.

메서드 인라인은 가상이 아닌 함수 (무시되지 않는 함수)에 대해서만 수행 할 수 있습니다. 'add'메서드가 하위 클래스에서 재정의되고 메서드가 포함 된 개체의 유형이 런타임까지 알려지지 않은 경우 어떤 일이 발생할지 고려하십시오. 이 경우 컴파일러는 인라인 할 메서드를 알지 못합니다. 그러나 메서드가 'final'로 표시된 경우 컴파일러는 하위 클래스에 의해 재정의 될 수 없기 때문에 인라인 일 수 있음을 쉽게 알 수 있습니다. 최종 메서드가 항상 인라인된다는 보장은 전혀 없습니다.

도달 할 수없는 데드 코드 제거

도달 할 수없는 코드는 가능한 실행 흐름에서 도달 할 수없는 코드입니다. 우리는 다음 예를 고려할 것입니다.

void foo() {
   if (a) return;
   else return;
   foobar(a,b); //unreachable code, compile time error
}

데드 코드도 도달 할 수없는 코드이지만이 경우 컴파일러는 오류를 내 보냅니다. 대신 경고 만받습니다. 생성자, 함수, try, catch, if, while 등과 같은 각 코드 블록에는 JLS (Java Language Specification)에 정의 된 도달 할 수없는 코드에 대한 자체 규칙이 있습니다.

일정한 접기

상수 접기 개념을 이해하려면 아래 예를 참조하십시오.

final int num = 5;
int b = num * 6; //compile-time constant, num never changes
//compiler would assign b a value of 30.

Java 개체의 수명주기는 JVM에서 관리합니다. 프로그래머가 객체를 생성하면 나머지 수명주기에 대해 걱정할 필요가 없습니다. JVM은 더 이상 사용되지 않는 객체를 자동으로 찾고 힙에서 메모리를 회수합니다.

가비지 수집은 JVM이 수행하는 주요 작업이며 필요에 맞게 조정하면 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 최신 JVM에서 제공하는 다양한 가비지 수집 알고리즘이 있습니다. 우리는 어떤 알고리즘을 사용할지 결정하기 위해 우리 애플리케이션의 필요성을 인식해야합니다.

C 및 C ++와 같은 비 GC 언어에서 할 수있는 것처럼 Java에서 프로그래밍 방식으로 객체를 할당 해제 할 수 없습니다. 따라서 Java에서 매달린 참조를 가질 수 없습니다. 그러나 널 참조 (JVM이 객체를 저장하지 않는 메모리 영역을 참조하는 참조)가있을 수 있습니다. 널 참조가 사용될 때마다 JVM은 NullPointerException을 발생시킵니다.

GC 덕분에 Java 프로그램에서 메모리 누수를 발견하는 경우는 드물지만 발생합니다. 이 장의 끝에서 메모리 누수를 만들 것입니다.

다음 GC는 최신 JVM에서 사용됩니다.

  • 직렬 수집기
  • 처리량 수집기
  • CMS 수집기
  • G1 수집가

위의 각 알고리즘은 더 이상 사용되지 않는 객체를 찾고 힙에서 차지하는 메모리를 회수하는 동일한 작업을 수행합니다. 이에 대한 순진한 접근 방식 중 하나는 각 객체가 가지고있는 참조 수를 계산하고 참조 수가 0이되는 즉시 해제하는 것입니다 (참조 계수라고도 함). 순진한 이유는 무엇입니까? 순환 연결 목록을 고려하십시오. 각 노드는 이에 대한 참조를 가지지 만 전체 객체는 어디에서나 참조되지 않으며 이상적으로는 해제되어야합니다.

JVM은 메모리를 해제 할뿐만 아니라 작은 메모리 척을 더 큰 척으로 통합합니다. 이것은 메모리 조각화를 방지하기 위해 수행됩니다.

간단히 말해서 일반적인 GC 알고리즘은 다음과 같은 활동을 수행합니다.

  • 사용하지 않는 개체 찾기
  • 힙에서 차지하는 메모리 해제
  • 조각 합치기

GC는 실행되는 동안 애플리케이션 스레드를 중지해야합니다. 이는 실행될 때 개체를 이동하므로 해당 개체를 사용할 수 없기 때문입니다. 이러한 중지를 '세계 중지 중지'라고하며 이러한 중지의 빈도와 지속 시간을 최소화하는 것이 GC를 조정하는 동안 우리가 목표로하는 것입니다.

메모리 통합

메모리 통합의 간단한 데모는 다음과 같습니다.

음영 처리 된 부분은 해제해야하는 개체입니다. 모든 공간이 회수 된 후에도 최대 크기 = 75Kb의 객체 만 할당 할 수 있습니다. 이것은 아래와 같이 200Kb의 여유 공간이있는 경우에도 마찬가지입니다.

대부분의 JVM은 힙을 3 세대로 나눕니다. the young generation (YG), the old generation (OG) and permanent generation (also called tenured generation). 그러한 생각의 이유는 무엇입니까?

경험적 연구에 따르면 생성 된 대부분의 물체는 수명이 매우 짧습니다.

출처

https://www.oracle.com

시간이 지남에 따라 점점 더 많은 객체가 할당됨을 알 수 있듯이 살아남는 바이트 수가 줄어 듭니다 (일반적으로). Java 객체는 사망률이 높습니다.

간단한 예를 살펴 보겠습니다. Java의 String 클래스는 변경할 수 없습니다. 즉, String 개체의 내용을 변경할 때마다 새 개체를 모두 만들어야합니다. 아래 코드와 같이 루프에서 문자열을 1000 번 변경했다고 가정 해 보겠습니다.

String str = “G11 GC”;

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

각 루프에서 새 문자열 객체를 생성하고 이전 반복 중에 생성 된 문자열은 쓸모 없게됩니다 (즉, 참조에 의해 참조되지 않음). 해당 객체의 수명은 한 번의 반복 일뿐입니다. GC에서 금방 수집합니다. 이러한 단기 개체는 힙의 젊은 세대 영역에 보관됩니다. 젊은 세대의 개체를 수집하는 과정을 마이너 가비지 수집이라고하며 항상 '세상을 멈추는'중지를 유발합니다.

젊은 세대가 꽉 차면 GC는 사소한 가비지 수집을 수행합니다. 죽은 개체는 폐기되고 살아있는 개체는 이전 세대로 이동됩니다. 이 프로세스 중에 애플리케이션 스레드가 중지됩니다.

여기에서 이러한 세대 설계가 제공하는 이점을 확인할 수 있습니다. 젊은 세대는 힙의 작은 부분 일 뿐이며 빠르게 채워집니다. 그러나 처리하는 데 걸리는 시간은 전체 힙을 처리하는 데 걸리는 시간보다 훨씬 적습니다. 따라서이 경우 '세계 중지'일시 중지는 더 자주 발생하지만 훨씬 짧습니다. 더 자주 멈출 수 있더라도 항상 더 긴 일시 중지보다 짧은 일시 중지를 목표로해야합니다. 이 튜토리얼의 뒷부분에서 자세히 설명하겠습니다.

젊은 세대는 두 개의 공간으로 나뉩니다. eden and survivor space. 에덴 수집 중에 살아남은 물체는 생존자 공간으로 이동하고, 생존자 공간에서 살아남은 자들은 구세대로 이동합니다. 젊은 세대는 수집되는 동안 압축됩니다.

개체가 구세대로 이동함에 따라 결국 채워지고 수집 및 압축되어야합니다. 다른 알고리즘은 이에 대해 다른 접근 방식을 취합니다. 그들 중 일부는 애플리케이션 스레드를 중지하고 (이는 이전 세대가 젊은 세대에 비해 상당히 크기 때문에 긴 '세계 중지'일시 중지로 이어짐), 일부는 애플리케이션 스레드가 계속 실행되는 동안 동시에 수행합니다. 이 프로세스를 전체 GC라고합니다. 그러한 두 수집가는CMS and G1.

이제 이러한 알고리즘을 자세히 분석해 보겠습니다.

직렬 GC

클라이언트 클래스 머신 (단일 프로세서 머신 또는 32b JVM, Windows)의 기본 GC입니다. 일반적으로 GC는 다중 스레드가 많지만 직렬 GC는 그렇지 않습니다. 힙을 처리하는 단일 스레드가 있으며 마이너 GC 또는 메이저 GC를 수행 할 때마다 애플리케이션 스레드를 중지합니다. 플래그를 지정하여 JVM에이 GC를 사용하도록 명령 할 수 있습니다.-XX:+UseSerialGC. 다른 알고리즘을 사용하려면 알고리즘 이름을 지정하십시오. 이전 세대는 주요 GC 중에 완전히 압축됩니다.

처리량 GC

이 GC는 64b JVM 및 다중 CPU 시스템에서 기본값입니다. 직렬 GC와 달리 여러 스레드를 사용하여 젊은 세대와 이전 세대를 처리합니다. 이 때문에 GC는parallel collector. 플래그를 사용하여 JVM에이 수집기를 사용하도록 명령 할 수 있습니다.-XX:+UseParallelOldGC 또는 -XX:+UseParallelGC(JDK 8 이상). 애플리케이션 스레드는 메이저 또는 마이너 가비지 콜렉션을 수행하는 동안 중지됩니다. 직렬 수집기와 마찬가지로 주요 GC 중에 젊은 세대를 완전히 압축합니다.

처리량 GC는 YG와 OG를 수집합니다. 에덴이 가득 차면 컬렉터는 라이브 오브젝트를 OG 또는 생존 공간 중 하나로 배출합니다 (아래 다이어그램의 SS0 및 SS1). 죽은 물체는 그들이 차지한 공간을 확보하기 위해 폐기됩니다.

YG의 GC 이전

YG의 GC 이후

전체 GC 동안 처리량 수집기는 전체 YG, SS0 및 SS1을 비 웁니다. 작업 후 OG에는 라이브 오브젝트 만 포함됩니다. 위의 두 수집기는 힙을 처리하는 동안 응용 프로그램 스레드를 중지합니다. 이는 주요 GC 중에 '세계를 멈추는'긴 일시 중지를 의미합니다. 다음 두 알고리즘은 더 많은 하드웨어 리소스를 사용하여이를 제거하는 것을 목표로합니다.

CMS 수집기

'동시 마크 스윕'을 의미합니다. 그 기능은 백그라운드 스레드를 사용하여 이전 세대를 주기적으로 스캔하고 죽은 개체를 제거하는 것입니다. 그러나 마이너 GC 중에 애플리케이션 스레드가 중지됩니다. 그러나 일시 중지는 매우 적습니다. 이것은 CMS를 낮은 일시 중지 수집기로 만듭니다.

이 콜렉터는 애플리케이션 스레드를 실행하는 동안 힙을 스캔하려면 추가 CPU 시간이 필요합니다. 또한 백그라운드 스레드는 힙을 수집하고 압축을 수행하지 않습니다. 힙이 조각화 될 수 있습니다. 이 과정이 계속되면서 특정 시점이 지나면 CMS는 모든 애플리케이션 스레드를 중지하고 단일 스레드를 사용하여 힙을 압축합니다. 다음 JVM 인수를 사용하여 JVM이 CMS 수집기를 사용하도록 지시하십시오.

“XX:+UseConcMarkSweepGC -XX:+UseParNewGC” CMS 수집기를 사용하도록 지시하는 JVM 인수로.

GC 이전

GC 이후

수집은 동시에 수행됩니다.

G1 GC

이 알고리즘은 힙을 여러 영역으로 나누어 작동합니다. CMS 콜렉터와 마찬가지로 마이너 GC를 수행하는 동안 애플리케이션 스레드를 중지하고 백그라운드 스레드를 사용하여 애플리케이션 스레드를 계속 유지하면서 이전 세대를 처리합니다. 구세대를 지역으로 나누었 기 때문에 객체를 한 지역에서 다른 지역으로 이동하면서 계속 압축합니다. 따라서 조각화가 최소화됩니다. 다음 플래그를 사용할 수 있습니다.XX:+UseG1GC이 알고리즘을 사용하도록 JVM에 지시하십시오. CMS와 마찬가지로 힙을 처리하고 애플리케이션 스레드를 동시에 실행하려면 더 많은 CPU 시간이 필요합니다.

이 알고리즘은 여러 지역으로 나뉘는 더 큰 힙 (> 4G)을 처리하도록 설계되었습니다. 그 지역 중 일부는 젊은 세대를 구성하고 나머지는 노인을 구성합니다. YG는 전통적으로 사용하여 지워집니다. 모든 애플리케이션 스레드가 중지되고 이전 세대 또는 생존자 공간에 여전히 살아있는 모든 개체가 삭제됩니다.

모든 GC 알고리즘은 힙을 YG와 OG로 나누고 STWP를 사용하여 YG를 정리합니다. 이 프로세스는 일반적으로 매우 빠릅니다.

지난 장에서 우리는 다양한 Generational Gcs에 대해 배웠습니다. 이 장에서는 GC를 조정하는 방법에 대해 설명합니다.

힙 크기

힙 크기는 Java 애플리케이션의 성능에 중요한 요소입니다. 너무 작 으면 자주 채워 지므로 GC에서 자주 수집해야합니다. 반면에 힙의 크기를 늘리기 만하면 수집 빈도는 줄어들지 만 일시 중지 시간은 늘어납니다.

또한 힙 크기를 늘리면 기본 OS에 심각한 불이익이 있습니다. 페이징을 사용하여 OS는 응용 프로그램이 실제로 사용 가능한 것보다 훨씬 많은 메모리를 볼 수 있도록합니다. OS는 디스크의 일부 스왑 공간을 사용하여 프로그램의 비활성 부분을 복사하여이를 관리합니다. 이러한 부분이 필요할 때 OS는 디스크에서 메모리로 다시 복사합니다.

머신에 8G의 메모리가 있고 JVM이 16G의 가상 메모리를보고 있다고 가정하면 JVM은 실제로 시스템에서 사용 가능한 8G 만 있다는 것을 알지 못할 것입니다. OS에서 16G를 요청하고 해당 메모리를 확보하면 계속 사용합니다. OS는 많은 데이터를 안팎으로 교환해야하며 이는 시스템에 엄청난 성능 저하를 초래합니다.

그런 다음 이러한 가상 메모리의 전체 GC 중에 발생하는 일시 중지가 발생합니다. GC는 수집 및 압축을 위해 전체 힙에서 작동하므로 가상 메모리가 디스크에서 스왑 될 때까지 많은 시간을 기다려야합니다. 동시 수집기의 경우 백그라운드 스레드는 데이터가 스왑 공간에서 메모리로 복사 될 때까지 많은 시간을 기다려야합니다.

따라서 최적의 힙 크기를 결정하는 방법에 대한 질문이 있습니다. 첫 번째 규칙은 실제로 존재하는 것보다 더 많은 메모리를 OS에 요청하지 않는 것입니다. 이것은 빈번한 스와핑에 대한 문제를 완전히 방지합니다. 머신에 여러 JVM이 설치되어 실행중인 경우 이들을 모두 결합한 총 메모리 요청은 시스템에있는 실제 RAM보다 적습니다.

두 개의 플래그를 사용하여 JVM의 메모리 요청 크기를 제어 할 수 있습니다.

  • -XmsN − 요청 된 초기 메모리를 제어합니다.

  • -XmxN − 요청할 수있는 최대 메모리를 제어합니다.

이 두 플래그의 기본값은 기본 OS에 따라 다릅니다. 예를 들어, MacOS에서 실행되는 64b JVM의 경우 -XmsN = 64M 및 -XmxN = 최소 1G 또는 총 물리적 메모리의 1/4입니다.

JVM은 두 값 사이에서 자동으로 조정할 수 있습니다. 예를 들어 GC가 너무 많이 발생하는 것을 발견하면 -XmxN 미만이고 원하는 성능 목표를 충족하는 한 메모리 크기를 계속 늘립니다.

애플리케이션에 필요한 메모리 양을 정확히 알고있는 경우 -XmsN = -XmxN을 설정할 수 있습니다. 이 경우 JVM은 힙의 "최적"값을 파악할 필요가 없으므로 GC 프로세스가 좀 더 효율적이됩니다.

세대 크기

YG에 할당 할 힙의 양과 OG에 할당 할 힙의 양을 결정할 수 있습니다. 이 두 값은 다음과 같은 방식으로 애플리케이션의 성능에 영향을줍니다.

YG의 크기가 매우 크면 수집 빈도가 줄어 듭니다. 이로 인해 OG로 승격되는 개체 수가 줄어 듭니다. 반면에 OG의 크기를 너무 많이 늘리면 수집하고 압축하는 데 너무 많은 시간이 걸리고 STW 일시 중지가 길어질 수 있습니다. 따라서 사용자는이 두 값 사이의 균형을 찾아야합니다.

다음은 이러한 값을 설정하는 데 사용할 수있는 플래그입니다.

  • -XX:NewRatio=N: OG에 대한 YG의 비율 (기본값 = 2)

  • -XX:NewSize=N: YG의 초기 크기

  • -XX:MaxNewSize=N: YG의 최대 크기

  • -XmnN: 이 플래그를 사용하여 NewSize 및 MaxNewSize를 동일한 값으로 설정하십시오.

YG의 초기 크기는 주어진 공식에 의해 NewRatio의 값에 의해 결정됩니다.

(total heap size) / (newRatio + 1)

newRatio의 초기 값이 2이므로 위의 공식은 YG의 초기 값이 전체 힙 크기의 1/3이되도록합니다. NewSize 플래그를 사용하여 YG의 크기를 명시 적으로 지정하여이 값을 항상 재정의 할 수 있습니다. 이 플래그에는 기본값이 없으며 명시 적으로 설정하지 않으면 위의 공식을 사용하여 YG의 크기가 계속 계산됩니다.

Permagen 및 Metaspace

permagen과 metaspace는 JVM이 클래스의 메타 데이터를 보관하는 힙 영역입니다. Java 7에서는이 공간을 'permagen'이라고하고 Java 8에서는 'metaspace'라고합니다. 이 정보는 컴파일러와 런타임에서 사용됩니다.

다음 플래그를 사용하여 permagen의 크기를 제어 할 수 있습니다. -XX: PermSize=N-XX:MaxPermSize=N. Metaspace의 크기는 다음을 사용하여 제어 할 수 있습니다.-XX:Metaspace- Size=N-XX:MaxMetaspaceSize=N.

플래그 값이 설정되지 않았을 때 permagen과 metaspace를 관리하는 방법에는 약간의 차이가 있습니다. 기본적으로 둘 다 기본 초기 크기를 갖습니다. 그러나 메타 스페이스는 필요한만큼의 힙을 차지할 수 있지만 permagen은 기본 초기 값만 차지할 수 있습니다. 예를 들어, 64b JVM에는 최대 영구 크기로 82M의 힙 공간이 있습니다.

지정하지 않으면 메타 공간이 무제한의 메모리를 차지할 수 있으므로 메모리 부족 오류가 발생할 수 있습니다. 이러한 영역의 크기가 조정될 때마다 전체 GC가 발생합니다. 따라서 시작하는 동안로드되는 클래스가 많은 경우 메타 스페이스의 크기가 계속 조정되어 매번 전체 GC가 생성 될 수 있습니다. 따라서 초기 메타 공간 크기가 너무 작은 경우 대규모 응용 프로그램을 시작하는 데 많은 시간이 걸립니다. 시작 시간을 줄이므로 초기 크기를 늘리는 것이 좋습니다.

permagen과 metaspace는 클래스 메타 데이터를 보유하지만 영구적이지 않으며 객체의 경우처럼 GC에서 공간을 회수합니다. 이는 일반적으로 서버 응용 프로그램의 경우입니다. 서버에 새 배포를 할 때마다 새 클래스 로더에 공간이 필요하므로 이전 메타 데이터를 정리해야합니다. 이 공간은 GC에 의해 해제됩니다.

이 장에서는 Java의 메모리 누수 개념에 대해 논의 할 것입니다.

다음 코드는 Java에서 메모리 누수를 생성합니다.

void queryDB() {
   try{
      Connection conn = ConnectionFactory.getConnection();
      PreparedStatement ps = conn.preparedStatement("query"); // executes a
      SQL
      ResultSet rs = ps.executeQuery();
      while(rs.hasNext()) {
         //process the record
      }
   } catch(SQLException sqlEx) {
      //print stack trace
   }
}

위 코드에서 메서드가 종료 될 때 연결 개체를 닫지 않았습니다. 따라서 물리적 연결은 GC가 트리거되기 전에 열린 상태로 유지되고 연결 개체에 연결할 수없는 것으로 간주됩니다. 이제 연결 개체에 대해 final 메서드를 호출하지만 구현되지 않을 수 있습니다. 따라서 개체는이주기에서 가비지 수집되지 않습니다.

원격 서버가 연결이 오랫동안 열려 있음을 확인하고 강제로 종료 할 때까지 동일한 일이 다음에 발생합니다. 따라서 참조가없는 객체는 오랫동안 메모리에 남아 누수가 발생합니다.