프로그래밍에 처음 입문했을 때에는 자바라는 언어가 그렇게 매력적으로 다가오진 않았다.
아무래도 프로그래밍 입문자 입장에서 봤을 때, 편이성 측면에서 동적 타입 언어인 Python에 비해 코드를 이해하고 사용하기에는 조금 더 불편하면서도, 성능 측면에서는 C나 C++에 비해 좋지 않아 애매한 포지션에 있는 언어라고 느꼈던 것 같다.
현재는 백엔드 개발에 있어서 Java를 주로 사용하고 있다. 현재 일하고 있는 회사에서 개발하고 있는 제품이 Java로 개발되어 있어서 Java 프로그래밍을 시작하게 되었지만 공부를 하면 할수록 Java만의 매력을 크게 느끼고 있다. 4년 전에 실리콘밸리에서 인턴쉽 프로그램을 하고 있을 때, 당시 LinkedIn Staff Data Engineer로 계셨던 멘토분께서 AI나 Data Science같은 유행에 휩쓸리지말고 CS와 개발 펀드멘탈을 먼저 키우고 나서 하고 싶은 것을 하라고 조언해주셨던 적이 있다. 그러면서 자료구조와 알고리즘을 공부할 때에는 C를 이용해서 깊게 해보고, OOP를 공부할 때에는 Java와 Spring을 깊게 해보라고 하셨는데 지금은 그게 어떤 의미인지를 조금은 알 것 같다.
여러 프로그래밍 언어 중에서도 Java는 OOP 개념에 가장 충실한 언어 중 하나라고 생각하며, 백엔드 개발자라면 한 번쯤은 깊게 공부해볼만한 가치가 있다고 생각한다.
흔히 알려진 Java의 특징은 크게 다음과 같다.
여기서 가장 큰 특징은 JVM 기반 언어라는 점이다.
사실 Java의 특징이나 장, 단점들은 대부분 JVM과 관련이 있다고 볼 수 있다.

Java 프로그램의 경우, 이를 실행하게 되면 OS 레벨과 App 레벨 사이에 존재하는 JVM이 실행되면서 OS로부터 필요한 메모리를 할당받고 그 위에서 Java 프로그램이 실행된다. 그 과정을 간단하게 풀어서 설명하자면,
1. 컴파일 타임에 javac(Java 컴파일러)가 자바 소스 코드인 .java 파일을 바이트 코드로 쓰여진 .class 파일, 즉 JVM을 위한 기계어로 변환한다.
2. 런타임에 JVM의 인터프리터가 동적으로 바이트 코드를 OS 혹은 Platform에 맞는 기계어로 변환해서 실행한다.
Java는 WORA(Write Once Run Anywhere) 철학을 가지고 개발되었는데 이것이 가능한 이유는 JVM이 런타임에 바이트 코드를 OS나 CPU 아키텍처에 맞는 기계어로 바꿔주기 때문이다. 그렇기 때문에 Java 프로그램의 경우 특정 OS나 하드웨어에 대한 의존성 없이 Java 코드로 작성되어 있으면 동일하게 실행이 가능하여 이식성이 높다는 장점이 있다.
또한 JVM은 Garbage Collection 기능을 제공하기 때문에 개발자의 메모리 관리 및 책임 이슈를 구조적으로 제거하여, C나 C++와는 달리 개발자가 코드 레벨에서 메모리 관리에 대해 직접 관여할 필요없이 비즈니스 로직에만 집중할 수 있도록 해준다.
그리고 Java는 소스코드를 JVM 수준에서 이해가능한 바이트 코드로 컴파일하는 과정을 거치는데, 이 과정에서 변수의 타입이 결정되기 때문에 동적 타입 언어에 비해 실행 속도가 빠르며 타입 에러로 한 문제점을 초기에 발견할 수 있어 타입의 안정성도 높은 등 정적 타입 언어의 장점을 가지고 있다.
Java는 JVM 기반에서 동작한다는 점으로 인해 여러 가지 장점들이 존재하지만, 반대로 JVM 기반에서 동작하기 때문에 여러 가지 고려해야할 사항들이 있다.
개발자는 기능 개발 뿐만 아니라 개발한 어플리케이션을 잘 운영하는 것이 중요한데, Java 어플리케이션을 운영하면서 발생하는 다양한 장애 상황을 해결하기 위해서는 기본적으로 JVM 구조에 대해 제대로 이해하는 것이 중요하다.
Java 어플리케이션은 JVM 기반에서 동작하기 때문에 Garbage Collector에 의해 메모리가 관리된다. 이 떄 메모리 누수가 발생할 수도 있는데 이러한 경우 JVM Heap 덤프 분석을 통해 트러블 슈팅을 하게 된다. 또한 Garbage Collection이 발생할 때는 GC 스레드를 제외한 모든 스레드가 멈추기 때문에 이로 인해 서버의 응답 속도가 느려질 수 있기 때문에 최악의 경우 GC를 튜닝하기도 한다. 뿐만 아니라 JVM에서 인지할 수 없어 Garbage Collector로 관리되지 않는 Native Memory 영역에서 누수가 발생할 수도 있다. 이 경우에도 JVM 레벨에서 예상되는 원인들을 먼저 추적하고 Linux OS 프로세스 레벨에서 트러블 슈팅하는 것이 현실적인 해결 방향이기 때문에 JVM 구조에 대한 이해는 필수적이다. 그렇기 때문에 Java 개발자라면 JVM에 대해 깊게 하는 것이 중요하다.
JVM 구조는 다음과 같다.

JVM은 크게 총 3 가지 구성 요소로 이루어져 있다.
Class Loader는 런타임에 클래스 파일을 동적으로 메모리에 로드하는 역할을 담당한다. 이러한 과정은 Loading, Linking, Initialization 각 단계를 거치면서 진행되며, 클래스가 최초로 참조되는 시점에 일어나게 된다.
Runtime Data Area는 JVM이 프로그램을 수행하기 위해 OS로부터 할당받는 메모리 영역이다. Runtime Data Area는 목적에 따라 5개의 영역으로 나뉜다.
프로그램 실행 중 특정 클래스가 사용되면 해당 클래스의 .class 파일을 읽고 분석한 뒤 해당 클래스 코드에 대한 정보를 Method Area에 저장한다.
사용자가 관리하는 클래스 인스턴스들이 저장되는 공간으로, 런타임에 객체를 new 연산을 통해 동적으로 생성하면 해당 인스턴스가 Heap 영역의 메모리에 할당되어 사용된다.
스레드 제어를 위해 사용되는 영역으로 새로운 스레드가 생성될 때마다 이에 대응되는 Stack이 생성된다. 각 스레드에서 메서드가 호출되면 메서드와 메서드 정보는 해당 Stack에 쌓이게 되며 메서드 호출이 종료 될때 Stack에서 제거 된다.
JVM은 Stack 기반으로 동작하는 Stack 기반 VM으로, Stack에서 Operand를 뽑아내서 이를 별도의 메모리 공간에 저장해서 명령을 처리하는 방식을 취하는데 이 때 사용되는 메모리 공간을 PC Register라고 한다. PC Register는 스레드가 생성될 때마다 생기는 공간으로 스레드가 어떤 명령을 실행하게 될지에 대한 정보를 기록한다.
메모리에 로드된 바이트 코드를 실행하는 Runtime Module로 Interpreter 방식과 JIT Compiler 방식을 혼합해서 사용한다.
위의 개념들을 바탕으로 Java 프로그램이 개발되고 실행되는 과정을 JVM 구성 요소들로 표현하면 다음과 같다.
