해당 글은 백기선님의 <더 자바, 코드를 조작하는 다양한 방법> 강좌를 기반으로 작성되었습니다.
JVM,JRE,JDK 구별하기
JVM(Java Virtual Machine)이란?
자바 바이트 코드를 실행하는 역할을 수행한다.
즉, 바이트코드를 해당 OS에 특화된 언어로 변환하여 실행한다.
바이트 코드를 실행하는 표준이자 특정 벤더(제공사 : oracle,aws 등등)가 구현 가능하다.
JRE(Java Runtime Enviroment)란?
간단하게 보면 JVM + 핵심 Library로 구성된 자바 어플리케이션을 실행 할 수 있도록 구성된 배포판이라 할 수 있다.(JVM의 실행 환경을 구축함)
쉽게 말해, JVM만 배포하지않고 JRE 기준으로 배포한다.
개발 관련 도구를 포함하지는 않는다.
JDK(Java Development Kit)란?
JRE + 개발에 필요한 툴로 구성됨
오라클은 자바 11부터 JDK만 제공하여서, JRE를 따로 제공하지 않는다.(서서히 둘의 구분이 사라질 예정..)
소스코드를 작성할때 사용하는 자바 언어는 플랫폼에 독립적
그래서 Write Once Run Anywhere
JDK 구조 파헤치기
클래스 로더 시스템
컴파일 후에 생성되는 .class 파일에서 바이트 코드를 읽어오고 메모리에 분산 저장시키는 역할 수행
로딩 : 클래스 읽어오는 과정
링크 : 레퍼런스를 연결하는 과정
초기화 : static 값들 초기화 및 변수에 할당
메모리(Runtime Data Area)
메모리 영역은 Runtime Data Area라고도 불리며, 이는 JVM이 프로그램을 수행하기 위해 OS로부터 할당받는 메모리이다.
Method 영역에는 클래스 수준의 정보(클래스 이름, 부모 클래스 이름, 메소드, 변수 등) 저장하며, Runtime Constant Pool이 존재한다.
논리적으로는 Method 영역은 Heap 영역에 포함된다. 동시에 GC의 대상이 되는 영역이다.
Heap 영역은 객체(동적으로 생성되는 인스턴스)를 저장한다.
Method 영역과 Heap 영역은 쓰레드 간의 공유가 가능한 영역이다.
해당 두개의 영역 제외한 모든 영역은 쓰레드마다 존재하는 영역, 즉 공유 불가 자원이다.
스택 영역(JVM Stack)에는 쓰레드마다 런타임 스택을 만들고, 그 안에 메소드 호출을 스택 프레임이라는 부르는 블럭으로 쌓는다. 해당 메소드가 호출이 종료되면 스택에서 제거하며, 쓰레드가 종료되면 런타임 스택도 사라진다.
PC Register 영역 : 쓰레드마다 쓰레드 내 현재 실행할 스택 프레임을 가르키는 포인터가 생성된다
Native method stack : Java 언어 외로 작성된 네이티브 언어를 위한 스택이 존재
실행 엔진(Execution Engine)
실행 엔진은 클래스를 실행시키는 역할을 수행하며, 클래스 로더가 바이트 코드를 런타임 데이터 영역에 배치시키고 이것이 실행엔진에 의해서 실행된다.
자바 바이트 코드를 기계어로 바꾸는 역할을 수행한다.
인터프리터(Interpreter) : 바이트 코드를 한줄 씩 기계어로 변경해서 느리다는 단점을 가진다.
JIT(Just In Time) 컴파일러 : 인터프리터의 효율을 높이기 위해서, 인터프러터가 반복되는 코드를 발견하면 JIT컴파일러로 반복되는 바이트 코드를 모두 네이티브 코드를 바꾼다. 그 다음부터 인퍼프리터는 네이티브 코드로 컴파일된 코드를 바로 변환한다.바이트 코드를 네이티브로 바꾸는 시간이 오래걸리는 단점이 있지만 해당 네이티브 코드는 캐시에 보관하기 때문에 다음 번에 재수행 시 속도가 상당히 빠르다.그래서 JIT 컴파일러를 사용하는 JVM은 내부적으로 해당 메서드의 호출 빈도를 파악하여 일정 수준 이상인 경우 컴파일한다.
GC(Garbage Collector) : 더 이상 참조되지 않는 객체를 모아서 정리한다.
Native Method Interface(JNI)
자바 어플리케이션에서 C,C⁺⁺,어셈블러로 작성된 함수를 사용 할 수 있는 방법 제공한다.
Native 키워드를 사용해서 메소드를 호출한다.
Class Loader 파헤치기
로딩,링크,초기화 순으로 진행된다.
Loading
클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들고 메소드 영역에 저장한다.
로딩이 끝나면 해당 클래스 타입의 Class객체를 Heap영역에 저장
이 과정은 부트 스트랩 클래스로더 -> 익스텐션(플랫폼) 클래스로더 -> 애플리케이션 클래스 로더 순으로 클래스를 찾으며 최종적으로 찾지 못할 시 Class Not Found Exception이 발생한다.
아래 코드와 결과를 보면 클래스 로더의 계층구조를 알 수있다.
public class App
{
public static void main( String[] args )
{
ClassLoader classLoader = App.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());//native-code로 구현되어서 null 출력
}
}
Linking
Verify, prepare, Resolve 순의 세 단계로 이루어져 있다.
Verify : .class 파일 형식이 유효한지 체크한다.
Prepare : 클래스 변수(static 변수)와 기본값에 필요한 메모리 준비한다.
Resolve : 선택가능한 단계로 Symbolic Memory Reference를 메소드 영역에 있는 실제 레퍼런스로 교체한다.