Java 언어의 특징과 JVM(Java Virtual Machine) 의 동작 원리를 이해할 수 있다.
Java는 리모콘, 냉장고, 전자레인지 등 가전 제품에 적용될 임베디드 소프트웨어를 작성하기 위해 개발된 언어입니다. Java가 등장하기 전까지도 C/C++와 같은 언어들을 사용하여도 충분히 작업 가능한 영역이었으나, 이들은 CPU나 OS환경이 변경 될 경우 그에 맞춰 다시 작업해주어야하는 어려움이 있었습니다.
당시 썬 마이크로시스템즈에서 근무하던 제임스 고슬링(James Gosling)은 이러한 문제점을 극복하기 위해 1991년 그린 프로젝트라는 이름으로 Java 언어를 개발하기 시작하였고, 1995년 Java 1.0을 발표하게됩니다.
이러한 Java 언어에는 몇가지 중요한 특징이 있습니다.
Java는 운영체제에 독립적인 언어입니다. Java로 작성된 프로그램은 하드웨어나 OS에 상관 없이 JRE(JVM + Java 표준 라이브러리)만 설치되어 있다면 어디서든 실행이 가능합니다.
Java는 객체지향 프로그래밍언어입니다. 이는 기존 절차지향 프로그래밍 언어와는 다르게 데이터를 추상화한 객체(Object)를 위주로 프로그램을 구현하게됩니다.
객체지향 언어는 코드를 재사용하여 반복되는 코드 작성을 줄이고, 보다 간결한 표현이 가능합니다. 때문에 잘 구현된 객체지향 프로그램은 코드를 보다 직관적으로 만들고 유지보수가 용이하게 합니다.
Java는 객체지향 언어의 특징인 추상화, 상속, 다형성, 캡슐화를 지원합니다.
이전에 사용되었던 C/C++과 같은 언어들은 프로그래머가 직접 메모리의 생성과 소멸을 관리해주어야했습니다. 이는 사용하지 않는 자원의 누수나 과도한 자원 할당등의 위험성이 있습니다.
Java는 Garbage Collector를 지원합니다. 이는 프로그래머가 메모리 자원을 관리할 필요 없이 스스로 사용하지 않는 메모리를 반납합니다.
Java의 핵심인 운영체제의 독립성은 JVM이 있기에 가능합니다. JVM이란 무엇이며 동작 원리는 무엇인지 알아보도록 합시다.
JVM, Java Virtual-Machine은 자바 코드를 실행하기 위한 가상 컴퓨터입니다.. Java는 인간 수준에서 이해할 수 있는 고급 언어기 때문에 자바 원시 코드(*.java)는 기계가 직접 이해할 수 없습니다.
Java compiler는 자바 원시 코드를 자바 바이트 코드(*.class)로 변환하는 역할을 하는데, JVM은 이 변환된 자바 바이트 코드를 인식해 실행할 수 있도록 기계가 이해할 수 있도록 번역하는 역할을 합니다.
자바 코드 실행 순서 (출처: Oracle)
자바 코드 실행 상세
개발자에의해 작성된 자바 소스 파일(*.java)은 아래와 같은 단계적 절차를 거쳐 실행됩니다.
개발자에 의해 작성된 자바 소스 파일(*.java)은 자바 컴파일러(java compiler)를 통해 중간 언어인 자바 바이트 코드(*.class)로 변환된다.
변환 된 자바 바이트 코드는 JVM의 클래스 로더(Class Loader)에게 전달된다.
클래스 로더는 전달받은 자바 바이트 코드의 실행에 필요한 클래스들을 런타임 데이터 영역(JVM 메모리 내 존재)에 올린다.
실행 엔진은 런타임 데이터 영역에 로드 된 자바 바이트 코드를 명령어 단위로 읽어들이며 실행한다.
다음으로 JVM 내부 구조에 대해 조금 더 자세하게 살펴봅시다.
Java는 컴파일 타임이 아닌 런타임에 클래스를 최조로 참조할 때 해당 클래스를 로드하고 링크하는 동적 로드의 특징을 갖습니다.. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더입니다.
클래스 로더의 특징
클래스 로더는 서로 다른 부모-자식 관계의 클래스 로더들로 이루어진 계층 구조로 생성됩니다.
클래스 로더 구조
위 그림과 같이 최상위 클래스 부트스트랩 클래스 로더를 시작으로 계층 구조로 이루어져있습니다.
각각의 클래스 로더들의 특징에 대해 알아봅시다.
클래스 로더가 클래스 로드 요청을 받게 되면 1)클래스 로더 캐시, 2)상위 클래스 로더, 3)자기 자신 순서대로 요청 받은 클래스의 존재 유무를 확인합니다.
클래스 로더 캐시를 시작하여 점점 상위로 올라가며 클래스를 찾는 과정은 부트 스트랩 클래스 로더를 확인 할 때까지 진행되며, 부트스트랩 로더에도 해당 클래스가 존재하지 않는다면 파일 시스템에서 찾는 과정을 계속 하게됩니다.
한 가지 주목해야할 사항은 클래스를 찾는 과정 도중에 요청받은 클래스가 발견되더라도 부트스트랩 클래스 로더는 확인 할 때 까지 과정은 계속해서 진행되며, 최상위 클래스 로더에서 발견된 클래스를 로드하게됩니다.
클래스 로더가 클래스 로드 요청을 받았을 때, 클래스를 확인하는 절차는 상위 클래스 로더로 올라가며 진행되지만, 이 때 하위 클래스 로더를 확인하는 것은 불가능하다는 특성이 가시성 제한입니다.
클래스를 로드하는 작업은 가능하지만 반대 작업인 언로드는 불가능합니다.
각각의 클래스 로더는 로드된 클래스를 보관하는 네임스페이스 공간을 갖습니다.
로드 할 클래스를 확인할 때 이미 로드 된 클래스인지 확인하기 위해 네임스페이스의 FQCN(Full Qualified Class Name)을 확인합니다.
FQCN이 같더라도 네임스페이스가 다르면 다른 클래스로 간주됩니다.
클래스 로더가 발견된 클래스를 로드하는 단계는 아래 그림과 같이 진행됩니다.
클래스 로드 단계
JVM이 실행되면서 OS로부터 할당 받은 메모리를 관리하는 곳이 런타임 메모리 영역입니다.
런타임 메모리 영역은 Method Area, Heap, JVM Stacks, PC Registers, Native Method Stacks로 구성되어있습니다.
이중 Method Area와 Heap영역은 모든 Thread가 공유(동시성 이슈에 고려 필요)하는 자원이고
JVM Stacks, PC Registers, Native Method Stacks는 각각의 Thread가 생성될 때 독립적으로 할당받는 자원입니다.
런타임 데이터 영역
각 요소에 대하여 좀 더 자세하게 알아보도록 합시다.
Method Area
Heap
JVM Stacks
PC Registers
Native Method Stacks
실행 엔진은 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어와 실행하는 역할을 합니다. Java의 실행 엔진은 바이트 코드를 기계가 실행 할 수 있는 언어로 번역하기 위해 인터프리터와 JIT 컴파일러 두 가지 방식으로 번역을 수행합니다.