업무 중 엔진을 패치하는 작업을 하게 되면, 자바 언어 자체의 기본적인 내용을 모르면 이해하지 못하는 경우가 종종 있었다... 그럴때마다 자바에 대한 기초적인 구조부터 공부할 필요가 있다고 생각하여 정리하는 시간을 가져보고자 한다!
JVM은 자바 바이트 코드(.class 파일)를 OS에 특화된 코드로 변환하여 실행하는 역할을 한다.
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
위의 자바 코드를 컴파일을 하면 클래스 파일이 나오는데, 클래스 파일을 보면 바이트 코드로 변환된 것을 확인할 수 있다.
Compiled from "HelloWorld.java"
public class HelloWorld {
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello, World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
그리고 '인터프리터와 JIT(Just In Time) 컴파일러'가 native OS에 맞춰서 바이트 코드(.class)를 실행해준다. 현재 나의 컴퓨터는 Mac OS를 사용하고 있으므로, Mac에 맞춰 machine 코드로 변경한 다음, machine이 이해할 수 있는 코드로 변환하여 실행해준다.
즉, JVM은 바이트 코드를 어떻게 실행할 수 있는지에 대한 스펙이라고 할 수 있다. 또한, JVM은 machine 코드로 바꿔서 실행해야 하는데 machine 코드라는게 OS에 맞춰서 실행해야 하기 때문에 특정 플랫폼에 종속적이라는 특징을 가지고 있다.
JVM은 홀로 제공되지 않고, 최소한의 배포 단위가 JRE이다.
JRE는 JVM과 함께 라이브러리를 제공하는 형태인데, 자바 어플리케이션을 실행하는데 필요한 것들만 들어있다. 자바 바이트 코드를 실행해야하므로 JVM이 들어있고, 자바의 핵심 라이브러리 및 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있다.
즉, JRE는 자바 어플리케이션이 실행될 수 있도록 구성되어 있는 배포판이라고 생각하면 된다. 하지만 자바를 개발하는 도구는 포함되어 있지 않다!
(jre를 다운받아보면 java는 들어있지만, 자바를 컴파일할 때 사용하는 javac는 들어있지 않다)
JDK는 JRE와 함께 자바 개발에 필요한 툴을 제공하는 형태이다.
우리같은 개발자들은 JRE를 설치하여 사용할 일보다 JDK를 설치할 일이 많다 !
하지만, 자바 11부터는 JRE를 따로 제공하지 않으며 JDK만 제공하고 있다. 그러므로 더 이상 헷갈릴 일은 없지만 ! 업무에서 사용하고 있는 자바 버전이 11보다 낮을 수도 있기 때문에 ^^.. ㅜ 잘 구분해두도록 하자.
(엔진 패치 작업을 진행할 때, 'JDK가 어쩌고 저쩌고~' 하는데 JRE랑 헷갈려서 머리가 어질어질했던 경험이 있다 ㅋ_ㅋ)
참고: https://www.geeksforgeeks.org/jvm-works-jvm-architecture/
JVM의 구조 크게 4가지 요소로 구성되어 있다. 공부할 때마다 매번 헷갈려 하는 개념이기 때문에(개념이 너무 생소하고 어렵다 ;_;) 정리해보고자 한다.
클래스 로더는 .class 파일에서 바이트 코드를 읽고 메모리에 적절하게 배치하는 역할을 한다.
클래스 로더는 크게 3가지 과정으로 나눌 수 있다.
메모리는 크게 5가지 영역으로 나누어져 있다.
'힙, 메소드' 영역은 전체 쓰레드가 참조할 수 있도록 공유되는 영역이다. 반면, '스택, PC, 네이티브 메소드 스택' 영역은 쓰레드 별로 공유되는 영역이다.
스택
- 쓰레드마다 런타임 스택을 만들고, 그 안에 메소드 호출을 '스택 프레임'이라는 블럭으로 쌓는다.
(스택 프레임 = 메소드 call)
- 그래서, 쓰레드가 종료되면 런타임 스택도 함께 사라진다.
PC(Program Counter): 쓰레드마다 쓰레드 내 현재 실행할 명령어의 위치를 가리키는 포인터
네이티브 메소드 스택: 자바 이외의 언어로 작성된 native 코드를 실행할 때 할당되는 영역이며, 타 언어의 스택 정보를 바이트 코드로 저장한다.
힙: 객체를 저장하는 영역
메소드: 클래스 수준의 정보를 저장하고 있는 영역
(클래스 수준의 정보? 클래스 이름, 부모 클래스 이름, 메소드, 변수)
바이트 코드를 이해하는 영역이다.
인터프리터: 바이트 코드를 native 코드로 바꿔서 machine이 이해할 수 있도록 한다. 한줄 씩 실행하면서 컴파일하기 때문에 효율적이지는 않다.
JIT(Just In Time) 컴파일러: 인터프리터의 효율을 높이기 위해, 인터프리터가 반복되는 바이트 코드를 발견하면 JIT 컴파일러로 반복되는 바이트 코드를 모두 native 코드로 바꿔둔다. 그 다음부터 인터프리터는 native 코드로 컴파일된 코드를 바로 사용한다.
GC(Garbage Collector): 메모리 최적화를 위해, 더 이상 참조되지 않는 객체를 모아서 정리한다.
자바 어플리케이션에서 자바 언어가 아닌 C, C++, 어셈블리 등으로 작성된 함수를 사용할 수 있는 방법을 제공하는 인터페이스를 말한다.
native
키워드를 사용한 메소드를 호출한다.
ex) Thread
의 currentThread()
메소드 - C로 구현되어 있으며, 소스코드를 확인하면 native
키워드가 붙어있다.
C, C++로 작성된 라이브러리이다. 해당 라이브러리는 꼭 JNI를 통해서 사용되어야 한다.
실제로 우리가 개발을 하게 되면 클래스 로더와 관련되어 있는 라이브러리와 툴을 경험할 기회가 많다. 그렇기에 클래스 로더 시스템에 구체적인 동작 방식에 대해 정리해보게 되었다.
클래스 로더는 로딩, 링크, 초기화 순으로 진행된다.
로딩 단계에서는 클래스 로더가 바이트 코드를 읽고 그 내용에 따라 적절한 바이너리 데이터를 만든다. 그리고 클래스 정보를 '메소드' 메모리 영역에 저장한다.
이때, 메소드 영역에 저장되는 데이터는 다음과 같다.
1) FQCN(Fully Qualified Class Name): 클래스가 속한 패키지명을 모두 포함한 이름 정보 -> 패키지 경로, 클래스 이름
2) 클래스인지/인터페이스인지/enum인지 구분하는 정보
3) 메소드와 변수 정보
로딩이 끝난 클래스는 해당 클래스 타입의 Class
객체를 생성하여 '힙' 메모리 영역에 저장한다.
클래스 로더는 계층 구조로 이뤄져 있으며 기본적으로 3가지 클래스 로더가 제공되고 있다.
JAVA_HOME₩lib
에 있는 코어 자바 API를 제공한다.JAVA_HOME₩lib₩ext
폴더 또는 java.ext.dirs 시스템 변수
에 해당하는 위치에 있는 클래스를 읽는다.-classpath
옵션 또는 java.class.path 환경 변수의 값
에 해당하는 위치)에서 클래스를 읽는다.로딩을 할 때, 최상위 부모 클래스 로더(Bootstrap)부터 바이트 코드를 읽어오려고 한다. 만약 부모 클래스 로더가 코드를 읽어오지 못하면, 자식 클래스 로드로 내려가면서 계층적으로 클래스 파일을 읽어오려고 한다.
만약 Application 클래스 로더까지 읽어오지 못한다면 Class Not Found Exception
이 발생하게 된다.
링크 단계에서는 크게 3가지 단계로 나누어져 있다.
마지막 단계인 초기화 단계에서, Prepare 단계에서 준비해둔 메모리 영역에 Static 변수의 값을 할당하게 된다.
static 블럭은 이 단계에서 실행되게 된다.
이 포스팅은 백기선님의 더 자바, "코드를 조작하는 다양한 방법"을 수강하고 정리한 내용입니다.