JVM
자바(Java) 언어의 실행 환경인 JVM(Java Virtual Machine)은 자바 바이트코드를 실행하는 가상 컴퓨터입니다. JVM은 말 그대로 가상 기계, 즉 가상의 컴퓨터를 물리적 컴퓨터의 메모리 안에 하나 더 구축합니다. 가상이긴 하지만 새로운 가상 컴퓨터를 구축하다보니 더 많은 물리적 CPU 자원과 메모리를 소비하기 때문에 당시 프로그래밍 언어들과 속도를 비교했을 때는 많이 느렸습니다. 하지만 하드웨어의 발전, 하드웨어 구성 요소의 가격 하락, 최적화된 알고리즘으로 개발된 API와 JVM으로 인해 현재는 자바의 성능이 느린 것을 체감하기는 힘듭니다.
자바의 가상 세계는 현실 세계에서 컴퓨터의 물리적인 하드웨어와 운영체제 그리고 그 위에서 구동될 소프트웨어를 가지는 구조를 모방하여 구성되어 있습니다.
Computer | JAVA | |
---|---|---|
소프트웨어 개발 도구 | JDK - 개발 도구 | JVM용 개발 도구 |
운영체제 | JRE - 실행환경 | JVM용 OS |
하드웨어 | JVM - 가상 기계 | 가상 컴퓨터 |
JVM에 대한 글을 작성하기 전에 먼저 자바에 대한 내용을 간단히 알아보겠습니다.
접기/펼치기
Java
자바는 1995년에 최초로 공개되었으며 현재 가장 널리 사용되는 프로그래밍 언어 중 하나로 다양한 플랫폼에서 사용할 수 있는 유연하고 강력한 프로그래밍 언어입니다. 자바는 “Write Once, Run Anywhere” 라는 슬로건을 내세워 여러 플랫폼에서 동일한 코드를 실행할 수 있는 것이 당시 프로그래밍 언어와의 큰 차이 중 하나였습니다. 자바는 웹 애플리케이션 개발을 위한 서블릿(Servlet)과 JavaServer Pages(JSP) 등의 기술을 도입하여 대규모 엔터프라이즈 시스템에서도 성능과 확장성을 보장하였습니다. 또한 자바는 객체지향 프로그래밍 언어로서의 특징도 가지며 자동 메모리 관리를 위한 가비지 컬렉션(Garbage Collection) 기능을 제공합니다.
특징
자바의 주요 특징은 다음과 같습니다.
객체 지향 프로그래밍(OOP: Object-Oriented Programming))
- 클래스(Class)와 객체(Object): 자바는 객체의 설계도인 클래스를 기반으로 생성된 실체인 객체를 생성하여 프로그램을 구성합니다.객체는 상태(state)와 동작(behavior)을 가지며, 데이터와 메소드로 구성됩니다.
- 캡슐화(Encapsulation): 자바는 캡슐화를 통해 데이터의 접근을 제어하고, 데이터의 무결성과 보안을 유지할 수 있습니다.
- 상속(Inheritance): 상속은 클래스 간에 부모-자식 관계를 형성하여 부모 클래스의 특성과 동작을 자식 클래스가 상속받을 수 있는 기능입니다. 상속을 통해 계층적인 구조를 만들어 객체의 관계를 표현할 수 있습니다.
- 다형성(Polymorphism): 다형성은 하나의 객체가 여러 가지 타입을 가질 수 있는 능력을 의미합니다. 자바에서는 다형성을 인터페이스와 상속을 통해 구현할 수 있습니다. 이를 통해 코드의 유연성과 확장성을 높일 수 있습니다.
- 추상화(Abstraction): 추상화는 객체의 공통적인 특성을 추출하여 모델화하는 과정입니다. 자바에서는 추상 클래스(abstract class)와 인터페이스를 활용한 추상화를 통해 복잡한 시스템을 단순화하고 구조화할 수 있습니다.
절차적/구조적 프로그래밍(PP: Procedure Programming, SP: Structured Programing)
객체 지향 프로그래밍은 절차적/구조적 프로그램의 많은 부분에서부터 유래되었습니다. 따라서 객체 지향 언어를 이해하는 데 절차적/구조적 프로그래밍을 이해하는 것은 큰 도움이 됩니다.
절차적 프로그래밍을 한마디로 표현하자면 goto
의 사용을 금한다는 것입니다. goto
를 사용하게 되면 프로그램의 실행 순서가 인간이 이해하기에는 복잡해 혼란을 야기합니다. 그러한 이유로 자바는 goto
를 예약어로 등록해 놓는 것뿐만 아닌 사용하지 못하게끔 선점해놓았습니다. 그리고 구조적 프로그래밍의 가장 큰 특징은 함수의 사용입니다. 함수는 중복 코드를 한 곳에 모아 관리 할 수 있고 논리를 함수 단위로 분리해서 이해하기 쉬운 코드를 작성할 수 있기 때문입니다. 추가로 공유 사용 시 문제가 발생하기 쉬운 전역 변수보다는 지역 변수 사용을 지향하라는 지침도 있습니다.
또한 자바가 지키지 못한 순수 객체 지향 언어의 특징은 아래와 같이 있습니다.
- 기본 자료형(Primitive Type)과 래퍼 클래스(Wrapper Class): 자바는 기본 자료형과 그에 대응하는 래퍼 클래스를 함께 제공합니다. 이로 인해 기본 자료형과 객체 간에 변환 작업이 필요하며, 객체 지향적인 특징이 상쇄될 수 있습니다. 또한 기본 자료형은 메모리 사용과 성능 면에서 유리하지만, 객체로 다루지 않기 때문에 순수한 객체 지향적인 접근이 어렵다는 한계가 있습니다.
- 정적(Static) 멤버: 자바는 정적 변수와 정적 메소드를 지원합니다. 이러한 정적 멤버는 인스턴스의 생성 없이 호출이 가능하며 정적 멤버의 사용은 전역적인 상태를 유지하고 공유할 수 있으므로 이는 객체 지향적인 설계 원칙과의 충돌을 야기할 수 있습니다.
- 절차 지향적인 요소: 자바에는 절차적인 흐름 제어 구조인 조건문(if-else, switch)과 반복문(for, while)과 같은 절차 지향적인 요소를 가지고 있습니다.
메모리 관리
자바는 가비지 컬렉션를 통해 더 이상 사용되지 않는 객체를 자동으로 식별하고 메모리에서 해제합니다. GC는 프로그램 실행 중에 동적으로 할당된 객체들을 추적하고, 참조되지 않는 객체들을 자동으로 정리하여 사용 가능한 메모리 공간을 유지합니다. 또한 자바의 객체는 동적으로 힙 메모리 영역에 할당됩니다. 힙 메모리는 GC의 관리 대상입니다. 객체가 더 이상 필요하지 않을 때 해당 객체와 그에 속한 메모리는 자동으로 회수되어 재사용 가능한 상태로 유지합니다.
JVM 구성
자바 개발 도구인 JDK(Java Development Kit)를 이용해 개발된 프로그램은 JRE(Java Runtime Environment)에 의해 가상 컴퓨터인 JVM 상에서 구동됩니다. 또한 배포 되는 JDK, JRE, JVM은 편의를 위해 JDK가 JRE를 포함하고 다시 JRE는 JVM을 포함하는 형태로 배포됩니다. 해당 내용을 그림으로 정리하면 아래와 같습니다.
JDK는 자바 소스 컴파일러(javac)를 포함하고 있고, JRE는 자바 프로그램 실행기인 java.exe를 포함하고 있습니다. 자바가 이런 구조를 택한 이유는 기존 언어로 작성한 프로그램은 각 플랫폼(하드웨어와 OS)용으로 배포되는 설치 파일을 따로 준비해야하는 불편함을 없애기 위해서입니다. 이런 구조는 개발자 본인이 사용 중인 플랫폼에서 설치된 JVM용으로 프로그램을 작성하고 배포하면 각 플랫폼에 맞는 JVM이 중재자로서 각 플랫폼에서 구동합니다. 이러한 구성 방식으로 인해 자바의 특성을 “Write Once Run Anywhere”이라고 합니다.
자바 프로그램을 실행시키면 JVM은 아래와 같은 단계를 거쳐 동작합니다.
- 컴파일: 자바 소스 컴파일러(javac)가 자바 소스 코드(.java)를 컴파일하면서 소스 코드의 구문 오류를 확인하고 자바 바이트 코드(.class)로 변환합니다.
- 클래스 로드: 컴파일 된 자바 바이트 코드를 JVM의 Class Loader를 통해 필요한 클래스 파일을 Runtime Data Area로 클래스 로드합니다.
- 메소드 영역 초기화: 클래스 로딩이 완료되면 Method Area을 초기화합니다. 클래스의 정적 변수(static variable)와 클래스 메소드(static method)가 메모리에 할당됩니다.
- main 메소드 실행: main 메소드는 프로그램의 진입점(entry point)으로, JVM은 main 메소드를 찾아 프로그램을 실행합니다.
- 스택 영역 생성: 스택 영역은 각 스레드마다 하나씩 존재하며 스레드가 시작될 떄 할당됩니다. 각각의 메소드 호출은 스택 프레임(stack frame)을 생성하여 메모리에 할당합니다.
- 바이트 코드 실행: JVM은 Execution Engine을 사용하여 바이트코드를 해석하고 실행합니다. Execution Engine은 바이트 코드를 하나씩 읽어서 해석하고, 인터프리터 또는 JIT 컴파일러( Just-in-Time Compiler)를 통해 실행합니다. Execution Engine은 프로그램의 흐름을 제어하며 바이트 코드를 실행합니다.
- 메모리 관리: JVM은 가비지 컬렉션를 통해 사용하지 않는 객체를 자동으로 정리합니다. 가비지 컬렉션는 힙 영역에서 메모리를 회수하여 재사용 가능한 상태로 만듭니다.
JVM 구조
위에서 JVM이 어떤 식으로 구성되어있는지, 자바 프로그램을 실행시키는 순서에 대해 설명했습니다. 지금부터는 JVM의 구조를 살펴보고 JVM의 각각의 요소들의 역활에 대해 알아 보겠습니다.
다음은 JVM의 구조를 상세하게 그린 도식입니다.
JVM은 크게 Class Loader SubSystem, Runtime Data Area, Execution Engine, JNI(Native Method Interface), Native Method Library 영역으로 나누어져있으며 각 부분에 대한 자세한 내용은 아래에서 작성하겠습니다.
Class Loader SubSystem
클래스 로더(Class Loader SubSystem)는 자바 바이트 코드(.class)를 로드하고 JVM의 메모리 영역인 Runtime Data Area로 동적 로딩하는 역활을 수행합니다. 클래스 로더는 필요에 따라 클래스 파일을 검색하고 로드하며, 로드된 클래스는 메모리 영역에 할당됩니다.
보다 자세한 내용은 해당 링크 참고 부탁 드립니다.
Runtime Data Area
Runtime Data Area는 OS로 부터 할당받은 JVM의 메모리 영역으로 자바 애플리케이션이 실행하는 동안 데이터를 저장하고 관리하는 영역입니다. Runtime Data Area는 총 5가지 영역으로 나뉘어져있으며 스레드 공유 여부에 따라 각 영역의 성격이 다릅니다.
보다 자세한 내용은 해당 링크 참고 부탁 드립니다.
Execution Engine
Execution Engine은 바이트 코드를 읽고 실제로 실행하는 역활을 담당합니다. Runtime Data Area에 있는 자바 바이트 코드를 읽고 운영체제에 맞게 기계어로 번경하여 해당하는 명령어(instruction) 단위로 실행합니다. Execution Engine은 위의 수행 과정에서 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행합니다.
보다 자세한 내용은 해당 링크 참고 부탁 드립니다.
Java Native Interface
JNI는 자바 언어와 네이티브 코드(C, C++ 등) 간의 상호 작용을 제공하는 프로그래밍 인터페이스입니다. JNI를 사용하면 자바 언어에서 네이티브 코드를 호출하고, 네이티브 코드에서 자바 언어의 기능을 사용할 수 있습니다.
JNI를 사용하여 자바 언어로는 접근하기 어려운 플랫폼 특정 기능을 활용할 수 있고 네이티브 코드를 작성하여 플랫폼별 라이브러리, 하드웨어 제어, 운영체제 기능 등을 사용할 수 있습니다.
Native Method Library
Native Method Library는 JVM에서 사용되는 네이티브 코드(C, C++ 등)로 구성된 라이브러리입니다. JVM은 운영체제와의 상호 작용, 네트워크 통신, 파일 시스템 접근, GUI 등과 같은 플랫폼 종속적인 기능을 지원해야 합니다. 이러한 기능은 네이티브 코드로 작성되어야 하기 때문에 Native Method Library에 해당하는 네이티브 라이브러리를 사용하여 구현됩니다.
오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.