우리가 흔히 code editor에서 작성하는 Java 언어는 컴퓨터가 이해할 수 없는 형태이다. 따라서 이를 컴퓨터가 이해할 수 있는 기계어로 변환을 시켜줘야 한다. 이때, 컴퓨터가 이해할 수 있는 기계어의 형식은 각 OS별로 다르므로 JVM이 중간에서 Java 코드를 각 OS가 이해할 수 있는 형식의 기계어로 바꿔 실행하는 데 도움을 준다.
JVM의 구조는 크게 4가지로 나뉜다.
Person.java 파일을 작성한 후 javac 명령어를 통해 컴파일하면 Person.class와 같이 .class 파일이 생성된다. 이 .class 파일은 java 코드를 바이트 코드로 변환한 파일이다. Class Loader는 이렇게 생성된 class 파일들을 엮어 JVM이 운영체제로부터 할당받은 메모리 영역인 Runtime Data Area에 적재하는 역할을 한다.
Class Loader에 의해 메모리에 적재된 바이트 코드들을 기계어로 변경하여 실행하는 역할을 한다. Interpreter 방식과 JIT(Just-In-Time) 컴파일러를 이용하는 방식이 있다. Interpreter 방식은 명령어 단위로 바이트 코드를 읽어 실행한다. Interpreter 방식이 한 줄씩 수행하기 때문에 느리다는 단점이 있어 이 단점을 보완하기 위해 JIT 컴파일러 방식이 도입되었다. JIT 컴파일러 방식은 자주 실행되는 바이트 코드들에 대해 전체를 컴파일 하여 네이티브 코드로 변경하는 방식이다. 네이티브 코드는 캐시에 보관되기 때문에 한 번 컴파일된 코드는 빠르게 수행이 가능하기 때문이다. 하지만 한 줄씩 컴파일하는 Interpreter 방식에 비해 바이트 코드 전체를 컴파일하는 JIT 방식의 속도가 훨씬 느린 탓에 Interpreter 방식과 JIT 방식을 혼용하는 것이 가장 효율적이다.
Heap 메모리 영역에 생성된 객체들 중 참조되지 않는 객체들을 탐색 후 제거하는 역할을 한다. GC가 수행되는 동안은 GC를 수행하는 쓰레드 외의 모든 쓰레드는 일시정지된다. 특히 Full GC가 발생하면 수 초간 모든 쓰레드가 정지하게 되어 장애로 이어지는 문제가 발생할 수 있다.
JVM의 메모리 영역으로 자바를 실행할 때 사용되는 데이터들을 적재하는 역할을 한다.
Thread가 시작될 때마다 생성되며 Thread마다 하나씩 존재한다. 현재 Thread가 실행되는 부분의 주소와 명령을 저장하고 있어 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 한다.
각종 형태의 변수나 임시 데이터, 스레드나 메소드의 정보를 저장한다. 할당했다가 바로 소멸시켜야 하는 특성을 가진 데이터를 저장하기 위한 영역이다. 메소드 호출 시마다 각각의 스택 프레임(메서드를 위한 공간)이 생성되고, 수행이 끝나면 프레임 단위로 삭제된다.
JAVA 언어가 아닌 다른 언어로 작성된 코드를 위한 공간이다. C/C++ 코드를 실행시켜 kernel에 접근할 수 있다.
모든 Thread가 공유하며, new 키워드로 생성된 객체와 배열이 생성되는 영역이다. GC가 참조되지 않는 메모리를 확인하고 제거하는 영역에 해당된다. 세 부분으로 나누어진다.
클래스 정보를 처음으로 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다. 클래스 멤버 변수의 이름, 데이터 타입, 접근 제어자 정보와 같은 필드 정보와 메소드 이름, 리턴 타입, 파라미터, 접근 제어자 정보 같은 메소드 정보, interface인지 class인지 나타내는 타입 정보가 저장되어 있다. 내부의 constant pool 에는 문자 상수, 타입, 필드, 객체 참조 등이 저장된다. static 변수, final class 변수 등이 생성되는 영역이다. 이때, 기본형이 아닌 static 클래스형 변수는 참조형만 저장되고 실제 인스턴스는 Heap에 저장되어 있다. 해당 인스턴스의 변수를 저장하기 위해서는 Heap에 메모리가 확보되어야 한다.
GC는 한정된 메모리 공간 내에서 새로운 객체의 할당을 위해 Heap 공간을 재활용하려는 목적을 갖는다.
새로 생성된 대부분의 객체는 Heap의 Eden 영역에 위치한다. Eden 영역에 객체가 가득 차게 되면 GC가 한 번 발생한다. 살아남은 객체는 Survivor 영역 중 하나로 이동시키고 Eden 영역에 남은 객체들을 제거한다. 이 과정을 계속 되풀이하는 과정에서 끝까지 살아남아 있는 객체는 일정시간 지속적으로 참조되고 있다는 의미이므로 Old 영역으로 이동한다.
Minor GC보다 시행 횟수가 적다. Old 영역에 객체가 가득 차게 되면 Old 영역의 모든 객체들을 검사하여 참조되지 않은 객체들을 한꺼번에 삭제한다. 그러면서 Heap 메모리 영역의 중간중간에 빈 메모리 공간이 발생하게 되는데 이 부분을 없애기 위해 재구성을 진행하게 된다. 메모리 재구성이 일어나는 도 중 다른 Thread에서 메모리를 사용해버리면 안되기 때문에 GC를 실행하는 Thread를 제외한 나머지 Thread의 작업이 모두 멈추게 된다. 이것을 'stop-the-world'라고 하며, GC 작업이 끝나야 중단되었던 작업을 재시작할 수 있다.
GC가 동작하기 전, 특정 객체가 garbage인지 아닌지 판단하기 위해 사용되는 개념은 reachability이다. heap 영역에 할당된 객체에 유효한 참조가 있다면 reachability, 없다면 unreachability로 판단한다. 객체들은 총 4가지 경우에 참조를 갖는다.
- 힙 내의 다른 객체에 의한 참조
- Java Stack으로부터의 참조
- Native Stack에 의해 생성된 객체에 대한 참조
- Method Area의 정적 변수에 의한 참조
Garbage Collection은 위와 같은 참조를 하고 있는 Reachable Object를 스캔하는 것을 Mark, Mark 되어있지 않은 모든 Object들을 Heap 영역에서 제거하는 것을 Sweep이라 하여 'Mark-and-Sweep'이라고도 한다.
[참조1] https://jeong-pro.tistory.com/148
[참조2] https://asfirstalways.tistory.com/158
[참조3] https://www.holaxprogramming.com/2013/07/16/java-jvm-runtime-data-area