Java - JDK , JRE, JVM , JIT 의미 파악 및 binary code / byte code 하는 방법 과 java compile 및 실행하는 방법

harold·2021년 1월 31일
0

java

목록 보기
12/13
post-thumbnail
post-custom-banner

JDK( Java Development Kit )


java 소스 작성 후
javac로 java code를 -> byte code로 변경하여 JVM이 읽을 수 있게하고
JRE를 실행시키는데(JRE를 포함) 필요한 class, main method를 로딩시킴


JRE( Java Runtime Environment )


java 프로그램을 단순히 실행하기 위해
.class 파일을 JRE의 java 프로그램으로 실행 시키는 소프트웨어 환경

JVM, class library, 실행 시스템, runtime환경 포함

출처 : JDK vs JRE vs JVM in Java – What’s the Difference ?


JVM( Java Virtual Machine )


java 프로그램이
interpreter와 JIT( Just In Time )을 사용하여
.java파일 -> .class파일로 byte code로 변환함으로써
모든 기기 및 운영체제에서 실행됨
( 특정 플랫폼에 종속됨 )

garbege collection을 수행하여 메모리를 관리함


JVM 구성요소

1. Class Loader ( Loading )

Runtime 시점에 .class에서 byte code를 읽고 메모리에 저장


class loader가 byte code를 읽는 두가지 경우

  1. byte code가 class를 static(정적)으로 참조할 때
    ( main method는 static이라 class loader가 가장 먼저 load함 )

  2. new 연산자 실행


class를 메모리에 잘못 올리면 프로그램을 종료하지 않고선
정보를 지우는 방법은 없음
만약, 변경된 클래스가 적용되게 하려면?

User Defined Class Loader 가 필요하다.
ex) Tomcat Class Loader 구조

class loader가 class를 찾지 못하면
ClassNotFound Exception 발생됨


class loader 구성 및 진행순서

  1. loading
    class를 읽어오는 과정

  2. link
    reference를 연결하는 과정

  3. initialization
    static 값들을 초기화 및 변수에 할당


동적으로 클래스를 로드하는
class.forName()메서드와 loadClass()메서드의 차이점

차이점대상방식
호출방식class.forName()class이름을 문자열로 받아와서 class를 로드
loadClass()ClassLoader 객체를 통해 class를 로드하고, class객체를 반환
ClassLoader 사용 방식class.forName()현재 실행 중인 클래스 로더 사용
loadClass()명시적으로 ClassLoader 객체를 사용하고, 해당 ClassLoader는 개발자가 직접 지정
초기화class.forName()로드된 클래스의 정적 블록 및 필드 초기화 수행
loadClass()클래스 로드 후 초기화 안함, 클래스 사용 전 명시적으로 초기화 해야 함
예외 처리class.forName()클래스를 찾지 못했을때 ClassNotFoundException 발생( 클래스 로드 과정에서 다른 예외도 발생될 수 있음 )
loadClass()클래스를 찾지 못했을때 ClassNotFoundException 발생

따라서
class.forName()
클래스 로더 계층 구조를 사용하고
초기화를 수행할때 사용

loadClass()
ClassLoader 객체를 명시적으로 사용하고
초기화를 수행하지 않는데 사용



2. Runtime Data Area ( JVM Memory )

  • JVM이 프로그램을 수행하기 위해 운영체제(OS)로 부터 필요한만큼 할당받는 메모리 공간 ( 물리적으로 RAM 이라 불림 ) 즉, java application을 실행할 때 사용되는 Data를 적재하는 영역

  • 이러한 OS로부터 받은 메모리 공간이 Runtime Data Area

Data 적재영역 구성

  • Static Area( Method Area, Class Area )

    • 하나의 .java 파일은 field, constructor , method로 구성, 이 중 filed 부분에서 선언된 변수( 전역변수 ) 와 정적 멤버변수( Static이 붙는 자료형 )의 데이터를 저장

    • Static 영역의 데이터는 프로그램 시작부터 종료될때까지 메모리에 남아있음, 즉 전역변수가 프로그램이 종료될때까지 어디서든 사용이 가능한 이유, 따라서 전역변수를 무분별하게 많이 사용하면 메모리가 부족할 우려가 있어서 필요한 변수만 사용해야 한다.

    • 모든 쓰레드가 공유하는 메모리 영역

    • 중요한 점은 하나의 JVM당 하나의 Method Area가 존재하며 자원을 공유한다.

    • class 파일의 byte code가 load되 곳
      ( JVM은 메인메소드를 호출하는 것으로 프로그램 시작 )

  • Stack Area

    • 모든 지역변수 데이터 값 저장
      ( 매개변수 , 블록문 내 변수 모두 포함 )

    • 메소드 호출 후 메모리에 할당되고 종료되면 메모리에서 해제 됨

    • thread 당 영역이 존재하기 떄문에 thread-safe하다.


Stack Area 예제 1

class Main 
{
  public static void main(String args[]) 
  {
    int a = 10 , b = 2, c = 12, d = 6;
		
	if( a > 0 && b > 0 && c > 0 && d > 0 )
	{
	    int sum = a + b + c + d;
	}
   
	System.out.println( " sum = " + sum );   // compile error		
   }
}

sum 변수는 Stack 영역에 할당되고 종료시 해제되므로 compile error가 발생한다.


Stack Area 예제 2

class Main
{
    public static void main(String args[])
    {
    	int sampleVal = 5;
        sampleVal = 4;
        sampleVal = 3;
	sampleVal = 2;
	sampleVal = 1;       
    	System.out.println( "sampleVal : " + sampleVal );
    }
}

에 5,4,3,2,1 순으로 값을 할당하였고 출력되는 값은 1이 출력된다.
즉 stack 영역은 LIFO ( Last In First Out )의 구조를 갖고 변수에 새로운 데이터가 할당되면 이전 데이터는 지워진다.

  • Heap Area

    • 모든 객체( 인스턴스 ), array가 저장됨

    • Stack 영역과 다르게 메소드 호출이 끝나고 사라지지 않고 유지됨.
      이때 변수( 객체, 객체변수, 참조변수 )는 Stack 영역의 공간에서 실제 데이터가 저장된 Heap 영역의 참조값을 new 연산자를 통해 return 받는다. 즉, 실제 데이터를 갖고 있는 Heap 영역의 참조 값을 Stack 영역의 객체가 갖고 있다
      ( 이렇게 return 받은 참조값을 갖고 있는 객체를 통해서만 해당 인스턴스를 핸들 할 수 있다. )

    • Method Area 와 마찬가지로 JVM 당
      하나의 영역이 존재하며, 자원을 공유하기 때문에 thread-safe하지 않다.

    • runtime시 동적으로 할당하여 사용하는 영역 class를 통해 instance를 생성하면 heap에 저장

    • Heap 에 저장된 데이터가 더 이상 사용이 불필요하다면 메모리 관리를 위해 JVM에서 자동으로 해제된다.

thread-safe 하지않다?
 - 멀티 스레드 프로그래밍에서 함수, 변수, 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻함

Heap Area 예제 1

class Main
{
	public static void main(String args[])
	{
		int[] ary = null;  			// stack 영역 공간 할당
		System.out.println( ary );	// 결과 : null
		
		ary = new int[5];			// heap 영역에 5개의 연속된 공간 할당 및 변수 ary에 참조값 할당
		System.out.println( ary );  // 결과 : @11db9923 ( 참조값 )
	}
}

ary 변수는 데이터가 저장된 heap 영역의 참조 값을 리턴 받아 갖고 있다.


Heap Area 예제 2

class Main
{
	public static void main(String args[])
	{
		String str1 = new String( "joker" );
		String str2 = new String( "joker" );
		
		if( str1 == str2 )	System.out.println( "같은 주소값 입니다" );
		else System.out.println( "다른 주소값 입니다" );
	}
}

String도 참조형이다. new 연산자를 이용해서 생성하면 데이터는 Heap 영역에 저장되고 str1 과 str2는 참조 값을 return 받는다. 저장된 주소가 다르기 때문에 "==" 로 비교 시 "다른 주소값 입니다." 가 출력 됨


PC Registers

  • thread가 생성될때마다 생기는 공간

  • 어떤 명령을 실행하게 될지에 대한 부분을 기록

  • JVM은 Stacks-Base 방식으로 작동하는데, CPU에 직접 instruction을 수행하지 않고, Stack에서 Operand를 뽑아내 이를 별도의 메모리 공간에 저장하는 방식을 취하는데, 이 메모리 공간이 PC Registers


Native Method Stack

  • java 외 언어로 작성된 native code를 위한 메모리 영역

  • 보통 C/C++등의 코드를 수행하기 위한 stack


3. Execution Engine

  • Load된 class의 byte code를 실행하는 runtime module

  • class loader를 통해 JVM 내에 runtime data area에 배치된 byte code는 Execution Engine에 의해 실행 ( byte code를 명령어 단위로 읽어서 실행 )


구성

  • Interpreter

    • byte code를 더 빨리 해석하지만 느리게 실행한다.

    • 단점은 하나의 메서드가 여러 번 호출 될 때마다 새로운 해석이 필요하다.

  • JIT Compiler

    • Interpreter의 단점을 보완

    • 반복된 code를 찾으면 전체 byte code를 compile하고 native code로 변경하는 JIT compiler를 사용한다. 이 native code는 반복되는 메소드 호출에 직접 사용되어 시스템 성능을 향상시킨다.

  • Garbage Collector

    • 참조되지 않은 객체를 모아서 제거한다.

    • System.gc()를 호출하여 수동으로도 할 수 있지만 실행이 보장되지는 않는다.

    • JVM의 GC는 객체가 생성되는것을 확인한다.

Full Garbage Collector가 일어날때는 모든 thread는 중단되는 것을 유의해야 한다. 
( STW : stop-the-world )
  • Stop-the-world 란?
    • GC를 실행하기 위해 JVM이 application 실행을 멈추는 것

    • stop-the-world가 발생하면 GC를 실행하는 thread를 제외한 나머지 thread는 모두 작업을 멈춤

    • Full GC가 발생하면 JVM은 Application 실행을 멈추고 GC를 실행하는 thread만 작동

    • 만약 WebServer에서 Full GC가 발생하면 서비스는 중단될 것이고 서비스가 중단 된 동안 각종 Time Out이 발생할 것이고 미뤄진 작업들이 누적되어 또 다른 Full GC를 발생시켜 장시간 장애가 발생할 수 있다.

  • Garbage Collector를 적게 사용 하기 위해선?
    • GC를 적게 하도록 하려면 객체 생성을 줄이는 작업을 먼저

    • String 대신 StringBuilder나 StringBuffer를 사용하는것을 생활화하는 것 부터 시작

그렇다면, 메모리 공간(heap)을 늘리면 Full GC를 피할 수 있나? 메모리 공간을 늘리면 Full GC의 첫 수행 시점은 늦출 수 있겠지만 STW의 시간은 heap 크기에 비례하기 때문에 메모리 공간이 클수록 더 많은 시간과 노력이 필요하다, 따라서 heap을 많이 할당하는 것이 반드시 좋은 것만은 아니다.


JVM에서의 메모리 관리

  • jvm 실행에 있어서 가장 일반적인 상호작용은 heap과 stack의 메모리 사용을 확인

  • 교정작업은 jvm의 메모리 설정 값들을 조율


JIT( Just In Time ) 컴파일러란 무엇이며 어떻게 동작하는가


  • 프로그램을 실제 실행하는 시점에 기계어로 번역하는 compile 기법

등장배경

java에 대한 성능 이슈로 인해 compile과 interpreter 방식을 모두 사용하는 java를 좀 더 빠르게 처리될 수 있도록 하기 위해 도입

이미 한번 읽어서 기계어로 변경한, 소스코드는 번역하지 않음

즉, 반복되는 코드를 모두 컴파일로 컴파일 시키는 것, 따라서 interpreter의 역학을 보조

프로그램을 만드는 방식 2가지

  • compile 방식

    • 소스코드를 한꺼번에 컴퓨터가 읽을 수 있는 native machine (기계)어로 변환

    • JIT compiler는 실행 시점에서 인터프리터 방식으로 기계어 코드를 생성하면서 그 코드를 캐싱하여, 같은 함수가 여러번 불릴때 매번 기계어 코드 생성을 방지한다.

    즉, java compiler가 java 프로그램 코드를 -> byte code로 변환한 다음 -> 실제 byte code를 실행하는 시점에서 JVM이 JIT 컴파일을 통해 byte code를 기계어로 번역한다.

    따라서 Interpreter의 단점을 보완, 전체 byte code를 compile, 캐시 사용으로 한번 compile하면 다음에는 빠르게 수행

  • interpreter 방식

    • 소스코드를 빌드시에 아무것도 하지 않고, runtime시 java byte code를 한줄씩 실행하며 변환, 여러번 실행하는 환경에선 다소 느림

JvmTest.java 소스 코드 생성 예제

  1. JvmTest.java 소스 코드를 작성

  2. javac.exe( java compiler )가 -> byte code ( JvmTest.class ) 로 변환

  3. JVM에서 각 운영체제에 맞는 기계어로 번역해서 전달

JIT 구조


그림을 보면 JIT compiler는 JRE 안에 존재

JRE는 JDK에 포함되고 JVM안에도 JIT가 존재

JIT컴파일러는 같은 코드를 매번 해석하지 않고 실행할 때 compile을 하면서 해당 코드를 캐싱해버림, 이후엔 바뀐 부분만 compile 후 나머지는 캐싱된 코드를 사용.

JIT 동작 방식


Java는 객체 지향 접근 방식을 따른다. 결과적으로 class가 JVM에 의해 실행되는 byte code로 구성

runtime에서 JVM은 class file을 로드하고 각각 적절한 계산이 수행
( interpreter시 java 응용 프로그램이 느린 경향이 있음 )

JIT compile은 runtime에 byte code를 -> 원시 기계 코드로 compile시켜 java 프로그램의 성능을 향상 시킨다.

JIT compiler는 메서드 호출 내내 활성화 된다.


binary code 와 byte code란 무엇인가?


binary code

  • 0 과 1로 구성

  • C언어로 작성 된 .C 파일을 compile한 .obj파일

  • compile한 .obj파일은 실제 컴퓨터가 이해하지 못하여 실행 불가

byte code

  • 특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법

  • 0과 1로 구성되어있는 이진코드이지만 binary code와 다르게 가상 머신이 이해할 수 있음

  • 하드웨어가 아닌 소프트웨어에 의해 처리되기 때문에 보통 기계어보다 더 추상적

  • 컴퓨터가 이해할 수 있는 기계어로 만들기 위해 인터프리터의 도움이 필요

결국, JVM을 사용하는 이유는 자바 프로그램 ( byte code )를 다양한 CPU환경에서 이식성 문제없이 실행할 수 있도록 도와주는 가상머신


컴파일 하는 방법


  • 프로그램 코드를 컴퓨터가 이해할 수있는 상태로 변환해주는 과정을 컴파일( compile ) 이라고하고 이 작업을 수행하는 software를 컴파일러( compiler ) 라고 한다.

  • java의 compiler는 javac라는 이름을 갖고 있다.

java 프로그램이 실행되는 과정

옵션

  • -classpath(cp)

    • compile하기 위해 참조할 .class파일들을 찾기 위해 file path를 지정

  • -sourcepath

    • compile하기 위해 필요로 하는 참조할 source file 위치를 지정
  • -d

    • class 파일을 생성할 root 디렉터리를 지정
    • 기본적으로 compiler는 -d 옵션을 주지 않으면, source file이 위차한 디렉터리에 .class 파일을 생성
  • -encoding

    • source file에 사용된 문자열 인코딩을 설정
    • 옵션 생략 시 플랫폼의 기본적인 컨버터가 사용
  • -g

    • 디버깅 정보를 생성
  • -nowarn

    • 경고 메시지를 생성시키지 않음
  • -verbose

    • 컴파일러와 링커가 현재 어느 소스파일이 컴파일 되고 있고, 어느 파일에 링크되고 있는지 정보 출력
  • - target

    • 지정된 java 버전의 JVM에서 작동되도록 .class 파일을 생성


실행하는 방법


  • compile을 실행하면 같은 디렉터리에 compile된 .class라는 이름의 파일이 생성됨

JvmTest.java compile 실행 예제

  • 경로
    java-study/src/main/java/com/basic/study/jvm/JvmTest.java

  • 방법

    1. javac명령어를 사용하여 compile 진행

    1. java명령어를 사용하여 프로그램 실행

옵션

  • -classpath(-cp)

    • 참조할 class파일 path를 짖어하는데, jar 파일, zip 파일, .class파일의 디렉터리 위치를 지정
  • jar

    • jar 파일로 압축되어 있는 java 프로그램을 실행

    • 정상적인 실행을 위해 jar파일 안의 MANIFEST.MF 파일의 Main-Class 가 작성되어 있어야 한다.



Reference Site

post-custom-banner

0개의 댓글