자바는 어떻게 작동하는가? (JVM, Class Loader, JVM Memory 등)
자바 컴파일러 (Java compiler)
자바 컴파일러는 자바를 가지고 작성한 자바 코드를 자바 가상머신(JVM)이 이해할 수 있는 자바 바이트코드로 변환하는 역할을 수행한다. '.java' 파일을 '.class' 파일로 변환하는 것이다.
자바 컴파일러는 자바를 설치하면 javac.exe 라는 실행 파일의 형태로 설치된다.
자바 바이트코드
바이트코드란?
특정 하드웨어가 아닌 가상 컴퓨터에서 작동하는 실행 프로그램을 위한 이진 표현법.
자바 바이트코드란 자바 가상 머신(JVM)이 이해할 수 있는 언어로 변환된 자바 소스 코드를 의미한다.
자바 바이트코드의 확장자는 .class 이며, 자바 바이트 코드는 자바 가상 머신(JVM)이 설치되어 있다면 어떠한 운영체제에서도 실행될 수 있다.
JVM
JVM(Java Virtual Machine)은 Java 응용 프로그램을 실행하는 런타임 엔진 역할을 한다.
자바 코드에 존재하는 메인 메소드를 호출하는 것이 바로 이 JVM이다.
JVM은 JRE(Java Runtime Environment)의 일부이다.
자바는 운영체제에 독립적(또는 WORA[Write Once Run Anywhere]라고도 함)이라고 하는데, 이것이 가능한 이유가 모두 JVM 덕분이다.
우리가 .java 파일을 컴파일할 때 .java파일에 존재하는 클래스 이름을 가진 .class 파일(자바 바이트코드)이 Java 컴파일러에 의해 생성되며, JVM이 .class 파일(자바 바이트코드)을 해석하고 실행한다.
Class Loader
클래스 로더의 하위 시스템
Loading(로딩), Linking(연결), Initialization(초기화)
주로 위 3가지 역할을 담당한다.
Loading
클래스 로더는 .class 파일을 읽고, 해당 파일에 대응하는 바이너리 데이터를 생성한 뒤 Method Area에 저장한다.
각각의 .class 파일에 대해 JVM는 메소드 영역에 다음 정보를 저장한다
- 로드된 클래스와 그의 부모(parent) 클래스의 정규화된 이름(The fully qualified name라 나와있는데 해석이..ㅠ)
- .class 파일이 Class, Interface, Enum과 관련이 있는지에 대한 여부
- 수정자, 변수 및 메서드 정보 등
.class 파일의 로딩이 끝난 후, JVM은 해당 .class 파일을 heap memory에 나타내기 위해 Class 타입의 객체를 생성한다.
해당 객체는 java.lang 패키지에 미리 정의된 Class 타입이라는 것을 주목하자.
이러한 Class 타입의 객체는 개발자가 클래스 수준의 정보(예를 들어 클래스의 이름, 부모 클래스의 이름, 메서드들과 변수들의 정보 등)를 얻어오는 데 사용될 수 있다.
우리는 해당 객체의 getClass() 메서드를 통해 객체 클래스의 정보를 얻을 수 있다.
Linking
검증(verification), 준비(preparation) 및 해결(resolution)을 수행한다.
- Verification: .class 파일의 정확성을 보장한다. 즉, 이 파일이 올바른 형식으로 지정되고 유효한 컴파일러에서 생성되었는지 여부를 확인한다. 확인에 실패하면 런타임 예외 java.lang.VerifyError가 발생한다. 이 활동은 ByteCodeVerifier 에 의해 수행되며, 이 활동이 완료되면 클래스 파일을 컴파일할 준비가 된 것이다.
- Preparation: JVM은 클래스 변수에 메모리를 할당하고 메모리를 기본값으로 초기화한다.
- Resolution: It is the process of replacing symbolic references from the type with direct references. It is done by searching into the method area to locate the referenced entity
- (번역이 조금 이상해서.. 아마 다음 뜻인것 같습니다) 인터페이스 참조를 실제 구현 클래스 참조로 바꾸는 과정이다. 참조된 엔터티를 찾기 위해 Method Area를 검색하여 수행됩니다.
Initialization
이 단계에서 모든 static 변수는 코드 및 static 블록(있는 경우)에 정의된 값으로 할당된다.
이것은 클래스에서는 위에서 아래로, 클래스 계층에서는 부모에서 자식으로 실행된다.
클래스 로더의 종류
일반적으로 3가지 클래스 로더가 있다.
- Bootstrap class loader : 구현된 모든 JVM은 반드시 bootstrap class loader를 가지고 있어야 하며, 이는 신뢰할 수 있는 클래스를 로드할 수 있다. Bootstrap class loader는 "JAVA_HOME/jre/lib" 디렉토리에 있는 핵심 자바 API 클래스를 로드한다. 이 경로는 일반적으로 부트스트랩 경로로 알려져 있다. C, C++과 같은 네이티브 언어로 구현된다.
- Extension class loader : Bootstrap class loader의 자식이다. 확장 디렉토리 "JAVA_HOME/jre/lib/ext"(확장 경로) 또는 java.ext.dirs 시스템 속성에 의해 지정된 기타 디렉토리에 있는 클래스를 로드한다. Java에서 sun.misc.Launcher$ExtClassLoader 클래스에 의해 구현된다.
- System/Application class loader : Extension class loader의 자식이다. 애플리케이션 클래스 경로에서 클래스를 로드하는 역할을 한다. 내부적으로 java.class.path에 매핑된 환경 변수를 사용한다. 또한 sun.misc.Launcher$AppClassLoader 클래스에 의해 구현된다.
// Java code to demonstrate Class Loader subsystem
public class Test {
public static void main(String[] args)
{
// String class is loaded by bootstrap loader, and
// bootstrap loader is not Java object, hence null
System.out.println(String.class.getClassLoader());
// Test class is loaded by Application loader
System.out.println(Test.class.getClassLoader());
}
}
Output
null
jdk.internal.loader.ClassLoaders$AppClassLoader@8bcc55f
JVM은 Delegation-Hierarchy 원칙에 따라 클래스를 로드한다.
시스템 클래스 로더는 확장 클래스 로더에 로드 요청을 위임하고, 확장 클래스 로더는 부트스트랩 클래스 로더에 요청을 위임한다. 부트스트랩 경로에서 클래스가 발견되면 해당 클래스가 로드되고 그렇지 않으면 요청이 확장 클래스 로더로 다시 전송된 다음 시스템 클래스 로더로 전송된다. 마지막으로 시스템 클래스 로더가 클래스를 로드하는 데 실패하면 런타임 예외 java.lang.ClassNotFoundException이 발생한다.
JVM Memory 영역을 알아보기 전에 PermGen과 Metaspace 영역에 대해 알아야 할 필요성이 있다.
PermGen Vs Metaspace
PermGen (permanent generatio)
Java 7 이하의 버전에서 존재한 영역으로, 클래스의 메타 데이터가 들어가는 공간이다.
또한 모든 static object가 이곳에 저장되었으며, 바이트 코드, JIT 정보에 대한 데이터, String Constant Pool도 이곳에 포함되었다.
PermGen은 메모리 영역의 크기가 고정되어 있으며 OutOfMemoryError를 발생시키는 원인이기도 하다.
Metaspace
Java 8 버전부터 새로 등장한 메모리 공간으로, PermGen 영역을 대체하는 공간이다.
Metaspace는 native memory(OS에서 애플리케이션에 제공하는 메모리)를 사용한다.
(static은 Java 7 이전에는 PermGen에 저장되었으나, Java 8 이후부터는 PermGen의 대체 공간인 Metaspace가 아닌, Heap 영역에 저장된다. 추가로 String Constant Pool도 Heap으로 옮겨졌다.)
(즉 클래스의 멤버변수(클래스변수와 인스턴스변수)는 Java 8 이후로 모두 Heap 영역에 저장된다.)
JVM Memory
Runtime Data Area
JVM이 프로그램을 수행하기 위해서 OS로부터 할당받는 메모리의 영역이다. 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재한다.
이 영역은 크게 Method Area, Heap Area, Strack Area, PC Register, Native Method Stack으로 나눌 수 있다.
노란색 글씨들인 우측 3개의 영역은 각 Thread 별로 생성이 되고, 좌측 2개의 영역은 모든 Thread가 공유한다.
Method area
모든 Thread가 공유한다.
Method area 영역에서는, 모든 클래스 수준의 정보(메타 데이터 - 클래스 이름, 부모 클래스 이름, 메서드들과 변수들 [멤버변수는 이제 모두 Heap 영역에 올라가므로 Method area에는 저장되지 않는다]등)가 저장된다.
(static이 Method area에 저장된다는 글이 있는데, 이는 Java 8 이전의 이야기이며, Java 8 이후부터는 Heap 영역에 저장된다.)
JVM 하나당 하나의 Method area를 가지며, 이곳의 자원들은 공유될 수 있다.
metaspace 영역에 속한다.(자바 8 이전에는 PermGen[permanent generation]영역에 속했었다.)
Heap area
모든 Thread가 공유한다.
모든 생성된 객체의 정보는 힙 영역에 저장된다.
인스턴스와 배열이 동적으로 생성되는 공간이자 GC(가비지 컬렉터)의 대상이 되는 영역이다.
모든 쓰레드에서 공유하므로 동기화 문제가 발생할 수 있다.
(힙 영역은 다양한 영역으로 세분화되어 구성되는데, 아래 링크에서 자세히 설명한다)
https://ttl-blog.tistory.com/368
Stack area
Thread별로 1개씩 존재한다.
지역 변수, 파라미터, 리턴 값, 연산에 사용되는 임시 값 등이 생성되는 영역입니다.
(heap 영역에 존재하는 객체들에 대한 참조와 primative type인 지역 변수들이 Stack area에 저장된다)
Thread의 Method를 호출할 때 위 수행 정보들이 Frame 이라는 단위로 JVM stack에 저장됩니다.
그리고 이렇게 스택 영역에 차례대로 저장되는 함수의 호출 정보를 스택 프레임(stack frame)이라고 한다.
Method 호출이 종료될 때 stack에서 제거됩니다.
참고. primitive type의 저장 위치
멤버변수(인스턴스변수, 클래스(static)변수)로 선언된 primitive type의 변수는 Heap에 저장되며, 지역변수(local variable)는 Stack에 저장된다.
PC Registers
Thread별로 1개씩 존재한다.
스레드의 현재 실행 명령의 주소를 저장한다. 각 스레드에는 별도의 PC 레지스터가 있다.
Native method stacks
Thread별로 1개씩 존재한다.
스레드 당 하나의 Native method stack을 가진다. Java외 다른 언어들로 작성된 Native method 정보를 저장한다.
Execution Engine
실행 엔진은 ".class"(바이트 코드)를 실행한다. 바이트 코드를 한 줄씩 읽고, 다양한 메모리 영역에 있는 데이터와 정보를 사용하고, 명령을 실행한다. 세 부분으로 분류할 수 있다.
인터프리터: 바이트코드를 한 줄씩 해석한 후 실행한다. 여기서 단점은 하나의 메소드를 여러 번 호출할 때 매번 해석이 필요하다는 것이다.
Just-In-Time Compiler(JIT) : 인터프리터의 효율성을 높이기 위해 사용한다. 전체 바이트 코드를 컴파일하여 네이티브 코드로 변경하여 인터프리터가 반복되는 메서드 호출을 볼 때마다 JIT에서 해당 부분에 대한 직접 네이티브 코드를 제공하므로 재해석이 필요하지 않아 효율성이 향상된다.
Garbage Collector: 참조되지 않은 객체를 파괴한다. (메모리의 회수)
Java Native Interface (JNI)
Native Method Libraries와 연동하여 실행에 필요한 Native Library(C, C++)를 제공하는 인터페이스이다.
JVM이 C/C++ 라이브러리를 호출하고 하드웨어에 특정한 C/C++ 라이브러리에서 호출할 수 있도록 한다.
Native Method Libraries
Execution Engine에서 필요로 하는 Native Libraries(C, C++)의 모음이다.
📔 Reference
https://www.geeksforgeeks.org/jvm-works-jvm-architecture/https://www.geeksforgeeks.org/jvm-works-jvm-architecture/
https://inspirit941.tistory.com/294
https://www.betsol.com/blog/java-memory-management-for-java-virtual-machine-jvm/
https://stackoverflow.com/questions/50163218/is-method-area-still-present-in-java-8
https://jgrammer.tistory.com/144
https://1-7171771.tistory.com/140
https://openjdk.java.net/jeps/122
https://mia-dahae.tistory.com/101
https://www.geeksforgeeks.org/metaspace-in-java-8-with-examples/
http://www.tcpschool.com/java/java_intro_programming