Java(또는 Kotlin)라는 언어를 해당 OS가 이해할 수 있는 언어로 변환하는 번역기라고 생각하면 편할 것 같다.
그 이유는 OS마다 System call이, 사용하고 있는 기기의 CPU마다 명령어 세트가 다르기 때문이다.
때문에 아래와 같이 운영체제따라, CPU에 따라 설치해야할 JVM이 다른 것이다.
(아래 사진은 JDK지만 JDK가 JVM을 포함하고 있다는 점을 감안해주길 바란다.)

따라서 독립적이라는 뜻은, 우리가 작성하는 Java 코드가 위에 같은 조건에 독립적이라는 뜻이고, 우리는 항상 Java라는 언어를 사용하고, 각 기기에는 그에 맞는 Java 번역기(JVM)를 설치해서 사용하는 것이다.
그럼 이것이 어떻게 가능할까?

*.java을 javac를 통해 컴파일하면 *.class(byte code)로 변환된다.*.class 파일은 아직 사람이 읽을 수 있는 코드 정도의 파일이다.*.class를 통해 Java Application을 실행하면, JVM에 Process가 생성되고, 이를 JVM 내부에 있는 Interpreter와 JIT compiler가 Native Machine Language(binary code)로 번역한다.*.class(byte code)와 번역된 Native Machine Language(binary code)를 Process의 메모리 공간 내부에 저장하게 되는 것이다.JVM이 대략 어떻게 작동하는지 알았으니, 다음으로 자세한 설명으로 넘어갈 차례이다.
해당 파트에서는 JVM을 구성하고 있는 요소들에 대해 설명해보겠다. (JVM 상자 내부에 것들)

Class loader는 Java Class File 형식에 부합하는 모든 것을 인식하고 JVM Process 메모리 공간에 로드하는 유닛이다.
(구현에 따라 다른 Binary 형식도 인식할 수 있지만, 최소 Class File 만은 반드시 인식한다.)
Class loader는 다음 순서를 지키며 작업을 실행한다.
코드를 해석(byte to binary)과 메모리 관리를 담당하는 유닛이다.
Byte code를 읽음과 동시에 번역하는 번역기다.
하지만, 이런 번역은 당연히 실행 시간에 영향을 줄 수 밖에 없다.
이를 해결하기 위해 JIT Compiler가 존재한다.
프로그램이 실행되는 도중에 실행되는 번역기다. (그래서 Just in time, JIT이다.)
이 Compiler는 주로 자주 실행되는 코드에 적용되어, 해당 코드를 Byte code로 미리 번역해둔다.
이런 작업은, 전체 실행 시간을 크게 단축시켜준다.
사용되지 않는 인스턴스를 찾아 메모리에서 제거하여 메모리 공간을 확보하는 역할을 한다.
이는 단순히 잘 사용 안한다고 제거해버리는게 아니라, 해당 구현에 정해져있는 정책을 따르는데, GC는 Java 버전별로 여러 구현을 가지고 있다.

해당 예시는 1개의 프로세스에서 3개의 Thread(Main Thread 포함)를 실행했을 때의 모습이다.
다음에 실행할 명령어의 주소를 저장하는 Register이다.
일반적으로 OS에서 모든 Thread는 CPU를 독점하지 않고 번갈아 사용하도록 되어 있으므로, Thread마다 PC Register를 가지고 되는 것이다.
프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역이다.
사실 이 부분은 JVM 설명에서 자세히 설명할만한 부분은 아니고, 이에 대해 자세한 내용은 구글에 stack memory, heap memory를 검색해보는 것을 추천한다.
Java 이외의 언어로 작성된 메서드(Native Method)를 지원하기 위해 존재하는 Stack이다.
이를 사용하지 않는 JVM을 구현한다면 제외되는 영역이다.
JVM이 시작될 때 생성되며, JVM의 모든 Thread가 공유하는 영역이다.
OS Process의 텍스트 영역과 유사한 역할을 하며, Runtime Constant Pool, 필드 그리고 메서드 데이터, 메서드 및 생성자의 코드와 같은 Class 별 구조가 저장된다.
이 영역은 논리적로 Heap 일부로 간주되지만, JVM 구현에 따라 해당 영역에 대한 Garbage Collecting 여부를 결정할 수 있다.
Class file에 있는 constant_pool table의 Class별 또는 Interface별 Runtime식 표현이다.
이는 여러 상수를 포함하는데, Compile 시점에 알려진 숫자(Numeric Literal)들 부터 Runtime 시점에서 해결해야되는 메서드 및 필드 참조에 이르기까지 다양한 종류의 상수가 포함된다.
Runtime Constant Pool은 전통적인 프로그래밍 언어의 Symbol Table과 유사한 기능을 수행하지만, 일반적으로 보다 더 넓은 범위의 데이터를 포함한다.

Method Area와 동일하게, JVM이 시작될 때 생성되며, JVM의 모든 Thread가 공유하는 영역이다.
또한, Heap은 모든 Class의 인스턴스와 배열에 대한 메모리가 할당되는 Runtime Data Area이라고도 할 수 있다.
해당 영역은 GC라는 자동 저장 관리 시스템에 의해 관리되는데, 위에서 언급했듯이, 자동방식은 구현에 따른다.
Java 17 버전 GC

위에 그림 1장으로 대부분의 설명을 할 수 있다.
JRE는 말 그대로 JVM에 실행하는데 필요한 라이브러리들을 포함한 Java를 실행시킬 수 있는 환경이다.
이것 없이 Java를 실행하려하면, JRE를 설치하라는 알림이 뜨는 프로그램이 종종있다. (필자의 경우, 어린 시절 Minecraft Java 버전이 그랬다.)
JDK는 JRE에다가 개발에 필요한 툴들을 추가로 포함시킨 것이다.
예를들어 javac(Java Compiler), jdb(Java Debugger), javadoc 등이 있다.
이것보다 훨씬 가볍게 정리하려고 시작한 글이였다...
그런데, 다른 분들의 블로그를 참고하면서, "Byte code는 Binary code가 되어 실행된다는데, 그럼 대체 어디에 저장이 되는걸까?", "JVM이랑 운영체제 과목에서 배웠던 Process는 무슨 관계지?"라는 의문이 계속 들더니 결국 좀 딥하게 들어가는 글이 되어버렸다 ㅎ..
하지만 나름 궁금한 점들을 많이 해결했고 그만큼 기억에도 남을 것 같아 만족!