Java - Java Basic JDK, JRE, JVM & JVM WarmUp

지피지기 백전불태


나의 수준을 알고, Java를 알면 실패하는 개발이 없다. 그 기본은 Java란 무엇인지를 알아야 한다.


1. JDK ( Java Development Kit )

1.1. JDK란?

  • JDK는 자바 개발키트의 약자로, 개발자들이 자바로 개발하는데 사용되는 SDK 키트이다.
  • 여기서 SDK는 Software Development Kit으로 하드웨어 플랫폼이나 OS 또는 프로그래밍 언어 제작사가 제공하는 툴이다. 즉 SDK를 활용하여 애플리케이션을 개발할 수 있는 것이고, JDK는 자바 개발용 SDK라고 생각하면 된다.
  • JDK 안에는 자바 개발에 필요한 development tool + 자바 실행에 필요한JRE를 포함하고 있다.


1.2. JDK 버전

  • 우리가 흔히 버전을 이야기 할 때, JDK 1.6 이런식으로 표기하는데 JDK가 가장 큰 범위라고 볼 수 있다. 그리고 Java 8 버전 이렇게 말하는게 결국은 JDK의 버전을 말하는 것이다. Java를 설치한다고 하는 것은 결국 JDK를 설치하는 것이다.
  • 현재는 버전을 표기하는 명칭이 Java SE, Java EE, Java ME, Java FX 등으로 나뉘면서 Java SE 몇 버전 이런식으로 버전을 표기한다.
  • 여기서 Java SE는 기본 우리가 아는 Java이며, Java EE는 대규모 기업용 확장판, Java ME는 작은 임베디드 기기를 위한 에디션, Java FX는 GUI 개발을 위한 에디션이다. 현재 주로 많이 쓰이는 건 역시 Java SE와 Java EE일 수 밖에 없다.


1.3. JDK 종류

  • JDK는 여러 종류가 있는데, 리눅스도 Ubuntu 나 RedHat 과 같이 나뉘어져있는 것처럼, Java도 소스코드가 오픈소스인 만큼 JDK도 여러 종류가 생겼다.
  • 이 중 주류는 Oracle JDK와 Open JDK, Amazon Corretto 등이 있다. Oracle JDK는 구독을 통해 유료 라이센스를 구매하지 않으면 사용할 수 없다.


1.4. Java Development Tool

  • JDK에는 JRE 뿐 아니라 개발 툴들이 들어있다.
  • JDK 디렉터리에 들어가면
    • 자바 개발, 실행에 필요한 도구가 들어있는 bin 폴더
    • 네이티브 코드 프로그래밍에 필요한 C언어 헤더 파일이 들어있는 include 폴더
    • 라이브러리 클래스들이 들어있는 lib 폴더 등이 있다.
  • 이 중 bin 폴더 안에는 여러가지가 있는데, 중요한 것 3가지만 외우면
    • 자바의 소스를 바이트 코드로 컴파일 해주는 javac.exe
    • 바이트 코드를 해석하고 실행하는 자바 인터프리터인 java.exe
    • 실행 중 오류를 찾는 디버거인 jdb.exe 가 있다.
    • 그 외에도 javadoc, jar, jmod, jlink 등 중요한 실행 도구가 있다.



2. JRE ( Java Runtime Environment )

  • JRE는 자바 실행환경이고, JVM과 자바 프로그램을 실행 시킬 때 필요한 API가 묶인 패키지이다.
  • 그 외에도 런타임 환경에 필요한 Propertis 세팅이나 jar 파일을 가지고 있다.
  • 즉, JDK는 Java를 개발하는 툴 + JRE까지를 모두 포함한거라면, JRE는 그 중 Java 프로그램을 실행시켜주는 패키지라고 보면 된다.



3. JVM ( Java Virtual Machine )

  • JVM은 자바 가상머신으로 자바를 돌리는 프로그램이라 생각하면 된다.
  • JRE가 프로그램을 실행하는데에 필요한 모든걸 담은 패키지라면, JVM은 그 중 순수 자바를 돌리는 프로그램이다.
  • Java의 특징을 보면 독립적인 플랫폼이라는 특징이 있다. 그 이유가 바로 JVM에 있다.
    • 이것이 Java가 인기 언어인 이유인데, JVM을 사용하면 Java 프로그램을 OS에 종속받지 않고, 모든 플랫폼에서 제약 없이 동작할 수 있게 한다.
      • 우선 일반 컴파일 기반의 프로그래밍 언어들을 보면, A 라는 환경에서 컴파일 된 기계 코드(바이너리 코드)를 B라는 환경에서는 읽을 수 없다.
      • 그러나 Java는 JVM이라는 것을 거쳐서 운영체제와 상호작용을 하기 때문에, JVM만 설치할 수 있는 환경이라면 어디서는 실행할 수 있다.
      • JVM은 javac에서 컴파일 되어 바이트 코드로 된 .class 파일을 읽어 해당 운영체제에 맞는 바이너리 코드로 변환해준다. 이게 핵심 이유이다.
      • 그래서 이전에는 다양한 OS 환경에 맞춰진 컴파일러가 이해할 수 있도록 직접 코드를 수정해야됐지만, Java는 그럴 필요가 없다는게 가장 큰 장점이다.


3.1. JIT 컴파일러

  • JIT 컴파일러는 JVM 안에 있으며, JVM의 단점을 보완해주는 역할을 한다.
    • Java의 실행 과정에서 보면 바이트 코드로 컴파일 된 .class 파일을 실행할 때마다 인터프리트 해줘야 하기 때문에 일반 컴파일 언어에 비해서 느리다는 단점이 있다.
    • 특히 코드가 바뀌면 해당 클래스만이지만 어쨋든 컴파일부터 인터프리트까지 다시 해줘야 한다.
    • JIT 컴파일러는 이런 단점을 캐싱으로 보완하였다.
      • JIT 컴파일러는 같은 코드를 매번 해석하는 것이 아니라, 자주 사용하는 메서드를 대상으로 해당 코드의 바이너리 코드를 캐싱하여 저장한다.
      • 그럼 이후에는 다시 인터프리트 하는 과정없이 캐싱된 저장소에서만 가져오면 되고, 바뀐 부분이 있다면 그 부분만 컴파일하고 나머지는 캐싱된 코드를 이용하는 것이다.
      • 이로 인하여 10배~20배 정도의 성능을 끌어올렸고 그만큼 단점 보완을 많이 하였지만, C언어의 속도를 따라잡지는 못하였다.
        • 속도를 중요시 하는 게임이나 임베디드에서 C를 사용하는 이유가 이것 때문이다.


3.2. GC (Garbage Collector)

  • JVM 안에는 GC 라는 것이 있다. 우리가 흔히 가비지 컬랙터라고 부르는데, 메모리 영역중 Heap에 해당하는 메모리에서 주소를 잃어버려 더이상 사용하지 않는 메모리를 자동으로 없애주며 관리한다.
  • 기존의 C언어나 C++은 메모리를 직접 해제해주여야 하지만, GC는 그것을 자동으로 해준다.



3.3 클래스 로더

  • 추후 더 자세히 알아보고, 우선은 클래스 등을 메모리에 올리는 역할을 한다 정도만 알아두자.

3.4 Runtime Data Area

  • JVM 안에는 Runtime Data Area 라고 해서 메모리 영역이 있다.
  • JVM 안의 클래스 로더라는 것이 클래스 파일을 코드 하면, Runtime Data Area의 알맞는 영역에 메모리가 할당된다.
    • [Native Method Stacks]
      • Object의 hashCode() 처럼 C언어로 개발된 Native Code가 들어있는 영역이며, 이런 네이티브 코드 호출을 관리하는 영역이다.
    • [PC Register]
      • 현재 스레드의 실행 주소가 담긴 영역이며, 현재 실행 중인 스레드가 다음에 실행할 명령어의 주소가 들어있다. 이걸로 프로그램의 흐름을 제어한다
      • 스레드마다 별도로 관리하기 때문에 JVM이 멀티 스레딩이 가능한 것이다.
    • [Method 영역 ( Class 영역 / Static 영역 )]
      • 메서드 영역, 스태틱 영역, 클래스 영역이라고 불린다.
      • JVM이 읽어들인 클래스와 인터페이스에 대한 Run Time 상수 풀, 멤버 변수, 클래스 변수(Static 변수), 생성자와 메서드등 바뀌지 않는 부분을 저장하는 공간이다.
      • 여기에 선언된 데이터는 프로그램 시작부터 종료까지 메모리에 남게된다.
      • static 메모리에 있는 데이터는 프로그램 종료전까지 어디서든 사용 가능하며, 무분별하게 많이 사용할 경우 메모리 부족 현상이 발생할 수 있다.
          public class Main {
              public static int a = 10;
        			   
              public static void main(String[] args) {
                  int a = 5;
              }
        			   
              public static void test(TestClass A) {
                  A.get();
              }
          }
        
          class TestClass {
              public int a = 100;
        			   
              public int get() {
                  return a;
              }
          }
        
      • Main Class의 int a와 main(String args[]) 그리고 test(TestClass A) 그리고 TestClass의 get()이 메서드 영역에 해당된다.
      • 매개 변수가 들어가는 것은 아니고, 메서드 자체가 들어가는 것이다.
    • [Stack 영역]
      • 메서드 내에서 정의하는 기본 자료형에 해당되는 지역변수 데이터 값이 저장된다. 주로 원시타입의 자료형 데이터에 해당하는 지역변수와, 매개 변수 데이터 값이 저장되며, Heap에 할당되는 값을 참조하는 객체의 주소 또한 Stack 영역에 생성된다.
      • 메서드 호출시 Stack 영역에 스택 프레임이 생기고, 그 안에 메서드를 호출한다.
      • 즉, Stack 영역 안에 하나의 메서드에 필요한 메모리 덩어리인 스택 프레임이라는 것이 생기며, 이 안에 메서드의 매개변수, 지역변수, 리턴값 등이 있다. 호출 범위가 종료되면 스택 프레임은 Stack에서 제거된다.
      • 그리고 Stack 영역은 Thread 마다 새로 생긴다.
      • 다른 영역은 Thread가 모두 공유하지만, Stack 메모리 공간은 침범할 수 없다.
          public class Main {
        			   
              public static void main(String[] args) {
                  int a = 5;
                  TestClass test = new TestClass();
                  test.a = 200;
              }
        			   
          }
        
          class TestClass {
              public int a = 100;
        			   
              public int get() {
                  return a;
              }
          }
        
      • 이런 코드가 있을 때, Stack에는 main이라는 스택 프레임이 생기고, 그 안에 int a와 test의 참조 주소가 순서대로 들어간다.
      • 그리고 해당 메서드가 종료되면 First In First Out에 따라 먼저 들어온 것부터 사라진다.
    • [Heap 영역]
      • 지막으로 Heap 영역이다. 이는 참조형 데이터 타입을 갖는 객체나 배열등이 저장되는 공간이다. 아까 이야기한 오브젝트들을 가리키는 참조 주소가 담긴 레퍼런스 변수들은 Stack에 생성된다.
      • Heap 영역은 JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역이다.
      • 우리가 Java의 특징 중 동적 로딩을 지원한다는 부분이 있는데 그 부분이 이 부분이다.
      • Heap은 메모리 호출이 끝나도 삭제되지 않고 유지되고 있다가, 그 어떤 참조 변수도 해당 인스턴스를 참조하지 않게 된다면 GC에 의해 청소가 된다
      • 그래서 바로 삭제되지 않고 조금 시간이 지난 후에 삭제가 되는 경우가 있다.
          public class Main {
        	   
              public static void main(String[] args) {
        			   
                  TestClass test = new TestClass();
                  int[] arr = { 1, 2, 3 };
        				  
              }
        			   
          }
        
          class TestClass {
              public int a = 100;
              public int b = 200;
        			   
              public int get() {
                  return a;
              }
          }
        
      • 해당 코드에서 Heap에 저장되는 걸 찾아보자.
      • 우선 new TestClass를 했기 때문에, test 라는 변수는 주소값을 가지고 stack에 쌓이게 된다. 그리고 Heap에는 TestClass 라는 인스턴스가 생기고, 그 안에 a = 100과 b = 200이라는 값을 넣어둔다.
      • 그리고 arr 이라는 배열이 생성되었다. arr이라는 참조 주소가 담긴 변수는 Stack에 저장되고, arr[0] arr[1] arr[2]에 해당되는 배열 주소과, 그 안의 값들은 Heap에 생성된다.


3.5 HotSpot JVM 이란?

  • 지금 우리가 사용하는 JVM이 HotSpot JVM 이다.
  • JDK 1.2부터 지원해줬는데, JIT 컴파일러를 통해 성능을 향상 시키는 것이 특징이다.
  • JIT에는 클라이언트모드와 서버모드가 있는데, 클라이언트 모드는 바이트 코드로부터 최대한 많은 정보를 뽑아내어 실제 동작하는 코드 블럭에 대한 최적화에 집중하며,
  • 서버 JIT는 코드보다는 전체적인 성능 최적화에 관심을 둔다.
  • 클라이언트 모드 : 바이트코드 표현 만들기, 중간표현식(LIR) 만들기, LIR을 이용하여 머신코드 생성
  • 서버 모드 - 죽은코드 삭제, 변수 끌어올리기, 전역 코드 이동, Null Check 삭제, 예외 처리 경로 최적화 등등
  • 2개 이상의 물리적 프로세서가 있거나, 2GB 이상의 물리적 메모리가 있을 경우 JVM은 자동적으로 서버 컴파일러를 선택하여 동작한다. 그러나, 우리가 명시적으로 지정하여 사용하고 싶을 때에는 java -server Class명 혹은 java -client Class명 이렇게 cmd에서 동작할 수 있다.



4. Java의 전체적인 실행 과정

  • 먼저 우리가 코드를 작성한 소스 파일은 .java 형태로 존재한다. 이걸 위에서 이야기한 javac.exe 라는 컴파일러를 통해 .class 파일로 컴파일을 해준다.
  • 이 때 컴파일 된 .class 파일은 바이트 코드가 들어가 있는데, 바이트 코드는 JVM에서 읽을 수 있는 언어이다. 즉 Java프로그래밍 언어와 바이너리 코드(기계 코드) 사이의 단계가 된 것이다. C나 C++은 컴파일 단계를 거치면 이미 바이너리 코드가 되어버리는데, Java는 컴파일 언어임과 동시에 인터프리트 언어임으로 이 과정을 갖는다.
  • 프로그램이 실행되면 JVM은 .class 파일을 Class Loader를 통해 읽어 들여 JVM에 올리고, 자바 API와 함께 실행시킨다.
  • 이렇게 JVM에 올라온 .class 파일들은 Execution Engine의 인터프리터나, JIT 컴파일러를 통해 바이너리 코드로 해석된다. 해석된 코드는 Runtime Data Area에 배치되어 실질적인 수행이 이루어진다.
  • 이 때 인터프리터는 코드를 한 줄 한 줄 바이너리 코드로 변환해주는 역할을 한다. 때문에 개발과 디버깅은 좋아도 속도가 컴파일 언어에 비해 현저히 느릴 수 밖에 없다. 그래서 나온 것이 JIT 컴파일러 이다.
  • 위에서 간단히 설명했지만 초기에는 인터프리터에 의해 시작되고, 자주 쓰이는 코드는 JIT 컴파일러에서 수행을 하게 된다. JIT컴파일러로 컴파일 된 코드를 “네이티브 코드” 라고 하는데 이는 캐시에 보관되기 때문에 빠르게 수행할 수 있다.



5. JVM Warm up

  • 그럼 결국 JIT 컴파일러에 올라오기 전까지는 느린 성능을 갖게 되는 것인 아닌가? 이러한 의문이 들 수 있다.
  • 그렇기에 등장한 것이 JVM Warm up 방법이다.
  • JVM Warm up은 말 그대로 JIT 컴파일러가 캐싱하는 영역에 미리 바뀐 바이너리 코드를 올려두는 것이다.
  • Warm up 방법에는 2가지가 있다.


하나는 사전에 수동으로 warm up을 하는 방법이다.

public class RandomArrayMain{

    static {
        System.out.println("static block start --");
        compareSearchTime();
        System.out.println("static block finish --\n");
    }

    public static void main(String[] args) {
        RandomArrayMain randomArrayMain = new RandomArrayMain();
        
        System.out.println("warm-up start --");
        for(int i=0; i<10; i++){
        	randomArrayMain.compareSearchTime();
        }
        System.out.println("warm-up finish --\n");
        
        System.out.println("measure start --");
        randomArrayMain.compareSearchTime();
        System.out.println("measure finish --");

    }

    public void compareSearchTime() {
    ...
    }
 }
  • 이렇게 측정할 메서드를 사전에 동일하게 반복 실행하면서 JIT 컴파일러가 최적화하기를 유도하면 JIT에 올라가게 된다.


다른 하나는 JMH 라는 OpenJDK에서 만든 warm up 용 라이브러리를 활용하는 것이다.


  • 여기서 주의해야 하는 점은, 일단 처음 애플리케이션이 기동할 때 어쨋든 Warm up 과정을 거친다.
  • 그래서 Warm up의 대상이 너무 많을 경우 초기 애플리케이션 기동 시간이 늦어질 수 있다.
  • 그러므로 기동시간과 최적화 사이의 밸런스를 잘 맞출 필요가 있다. ( 트레이드 오프 관계 )






results matching ""

    No results matching ""