자바가 탄생하기 이전에 객체지향 프로그램으로 자주 사용되었던 C++에는 큰 문제가 하나 있었다. 바로 운영체제로부터 독립적이지 못하다는 점이다. 쉽게 말해, Windows를 위해 만든 프로그램은 Windows에서만 작동이 가능했고, Mac OS에서 그 프로그램을 실행시키려면 Mac OS에 맞게 새로 프로그램을 만들고 컴파일해야 했다.
즉, 프로그래밍 언어가 운영체제에 종속적이기 때문에, 어떤 프로그램을 만들 때 각 운영체제 별로 따로 만들어주어야 했었다.
자바는 이러한 문제점을 해결하고자 탄생한 언어이다. 자바는 C++처럼 객체지향 프로그래밍이 가능하면서도, 운영체제로부터 독립되어있기 때문에, 자바로 소스 코드를 한 번만 작성하면 어떤 운영체제에서도 코드를 수정할 필요 없이 프로그램을 실행시킬 수 있다. 이와 같은 운영체제로부터의 독립성은 JVM이라는 별도의 프로그램을 통해서 구현된다.
JVM이란?
JVM(Java Virtual Machine)은 자바 프로그램을 실행시키는 도구이다. 즉, JVM은 자바로 작성한 소스 코드를 해석해 실행하는 별도의 프로그램인것이다.
프로그램이 실행되기 위해서는 CPU, 메모리, 각종 입출력 장치 등과 같은 컴퓨터 자원을 프로그램이 할당받아야 한다.
프로그램이 자신이 필요한 컴퓨터 자원을 운영체제에게 주문하면, 운영체제는 가용한 자원을 확인한 다음, 프로그램이 실행되는 데에 필요한 컴퓨터 자원을 프로그램에게 할당해주게 된다.
이 때, 프로그램이 운영체제에게 필요한 컴퓨터 자원을 요청하는 방식이 운영체제마다 다른데 이것 때문에 프로그래밍 언어가 운영체제에 대해 종속성을 가지게 되는 이유이다.
하지만 자바는 JVM을 매개해서 운영체제와 소통한다. 즉, JVM이 자바 프로그램과 운영체제 사이에서 일종의 통역가 역할을 수행하게 된다.
JVM은 각 운영체제에 적합한 버전이 존재한다. 즉, Windows용 JVM, Mac OS용 JVM, Linux용 JVM이 따로 존재한다.
이처럼 운영체제에 맞게 JVM이 개발되어져 있으며, JVM은 자바 소스 코드를 운영 체제에 맞게 변환해 실행시켜준다. 이것이 자바가 운영체제로부터 독립적으로 동작할 수 있는 이유이다.
JVM 구조
자바로 소스 코드를 작성하고 실행하면, 먼저 컴파일러가 실행되면서 컴파일이 진행된다. 컴파일의 결과로 .java 확장자를 가졌던 자바 소스 코드가 .class 확장자를 가진 바이트 코드 파일로 변환된다.
이후, JVM은 운영 체제로부터 소스 코드 실행에 필요한 메모리를 할당받는다. 그것이 바로 위 그림 상의 런타임 데이터 영역(Rumtime Data Area)이다.
그 다음에는 클래스 로더(Class Loader)가 바이트 코드 파일을 JVM 내부로 불러들여 런타임 데이터 영역에 적재시켜 자바 소스 코드를 메모리에 로드시킨다.
로드가 완료되면 실행 엔진(Execution Engine)이 런타임 데이터 영역에 적재된 바이트 코드를 실행시킨다.
이 때, 실행 엔진은 두 가지 방식으로 바이트 코드를 실행시킨다.
- 인터프리터(Interpreter)를 통해 코드를 한 줄씩 기계어로 번역하고 실행시키기
- JIT Compiler(Just-In-Time Compiler)를 통해 바이트 코드 전체를 기계어로 번역하고 실행시키기
실행 엔진은 기본적으로 1번의 방법을 통해 바이트 코드를 실행시키다가, 특정 바이트 코드가 자주 실행되면 해당 바이트 코드를 JIT Compiler를 통해 실행시킨다.
즉, 중복적으로 어떤 바이트 코드가 등장할 때, 인터프리터는 매 번 해당 바이트 코드를 해석하고 실행하지만, JIT 컴파일러가 동작하면 한 번에 바이트 코드를 해석하고 실행시킨다.
Stack과 Heap
JVM 메모리 구조
JVM에 Java 프로그램이 로드되어 실행될 때 특정 값 및 바이트코드, 객체, 변수등과 같은 데이터들이 메모리에 저장되어야 한다. 런타임 데이터 영역이 바로 이러한 정보를 담는 메모리 영역이며, 크게 5가지 영역으로 구분되어 있다.
이중 힙과 스택영역에 대해 알아보자.
Stack 영역
자료구조는 프로그램이 데이터를 저장하는 방식을 의미하는데 Stack은 그 자료구조 중 하나의 종류이다.
이러한 스택은 흔히 LIFO라는 성질을 갖는다. LIFO는 “Last In First Out”의 약자로, 마지막에 들어간 데이터가 가장 먼저 나온다는 의미이다.
가장 먼저 들어간 데이터를 맨 밑으로 들어오고 그위로 데이터들이 계속해서 쌓인다 이후 해당 데이터들이 필요할때 맨 위에 있는, 즉 맨 마지막에 들어왔던 데이터부터 꺼내게된다.
JVM안에서 Stack은 메서드가 호출되면 그 메서드를 위한 공간인 Method Frame이 생성되고 메서드 내부에서 사용하는 다양한 값들이 있는데 참조변수, 매개변수, 지역변수, 리턴값 및 연산시 일어나는 값들이 임시로 저장된다.
이런 Method Frame이 Stack에 호출되는 순서대로 쌓이게 되는데, Method의 동작이 완료되면 역순으로 제거된다.
Heap 영역
JVM에는 단 하나의 Heap 영역이 존재하며 JVM이 작동되면 이 영역은 자동 생성된다. 그리고 이 영역안에 객체나 인스턴스 변수, 배열이 저장된다.
Person person = new Person();
위의 예시에서 new Person()이 실행되면 Heap 영역에 인스턴스가 생성되며, 인스턴스가 생성된 위치의 주소값을 person에게 할당해주는데, 이 person은 Stack 영역에 선언된 변수이다.
즉, 우리가 객체를 다룬다는 것은 Stack 영역에 저장되어 있는 참조 변수를 통해 Heap 영역에 존재하는 객체를 다룬다는 의미가 된다. 정리하자면, Heap 영역은 실제 객체의 값이 저장되는 공간이다.
Garbage Collection
Garbage Collection
자바에서는 가비지 컬렉션이라는 메모리를 자동으로 관리하는 프로세스가 포함되어 있다. 가비지 컬렉션은 프로그램에서 더 이상 사용하지 않는 객체를 찾아 삭제하거나 제거하여 메모리를 확보하는 것을 의미한다.
Person person = new Person();
person.setName("김아무개");
person = null;
// 가비지 발생
person = new Person();
person.setName("이씨아저씨");
위의 예시 첫째 줄에서 참조 변수 person은 Person 클래스의 인스턴스의 주소값을 할당받고, 이어서 “김아무개”라는 문자열이 person이 가리키는 인스턴스의 name이라는 속성에 할당된다.
그런데, 세 번째 줄에서 참조 변수 person에 null이 할당됨으로써, 기존에 person이 가리키던 인스턴스와 참조변수 person 간의 연결이 끊어졌다.
프로그램이 실행 중일 때 이처럼 아무도 인스턴스를 참조하고 있지 않다면, 더 이상 메모리에 person이 가리키던 인스턴스가 존재해야 할 이유가 없다.
가비지 컬렉터는 이렇게 아무한테도 참조되고 있지 않은 객체 및 변수들을 검색하여 메모리에서 점유를 해제하며, 그럼으로써 메모리 공간을 확보하여 효율적으로 메모리를 사용할 수 있게 해주는 것이다.
동작 방식
JVM의 Heap 영역은 객체는 대부분 일회성이며, 메모리에 남아 있는 기간이 대부분 짧다는 전제로 설계되어 있다.
그렇기 때문에 객체가 얼마나 살아있냐에 따라서 Heap 영역 안에서도 영역을 나누게 되는데 Young, Old영역 이렇게 2가지로 나뉜다.
Young 영역에서는 새롭게 생성된 객체가 할당되는 곳이고 여기에는 많은 객체가 생성되었다 사라지는 것을 반복한다.
이 영역에서 활동하는 가비지 컬렉터를 Minor GC라고 부른다.
Old 영역에서는 Young영역에서 상태를 유지하고 살아남은 객체들이 복사되는 곳으로 보통 Young 영역보다 크게 할당되고 크기가 큰 만큼 가비지는 적게 발생한다.
이 영역에서 활동하는 가비지 컬렉터를 Major GC라고 부른다.
Young 영역과 Old 영역은 서로 다른 메모리 구조로 되어 있기 때문에, 세부적인 동작 방식은 다르지만 기본적으로 가비지 컬렉션이 실행될때는 다음의 2가지 단계를 따르는데
- Stop The World: Stop The World는 가비지 컬렉션을 실행시키기 위해 JVM이 애플리케이션의 실행을 멈추는 작업입니다. 가비지 컬렉션이 실행될때 가비지 컬렉션을 실행하는 스레드를 제외한 모든 스레드들의 작업은 중단되고, 가비지 정리가 완료되면 재개됩니다.
- Mark and Sweep: Mark는 사용되는 메모리와 사용하지 않는 메모리를 식별하는 작업을 의미하며, Sweep은 Mark단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업을 의미한다.
즉, Stop The World를 통해 모든 작업이 중단되면, 가비지 컬렉션이 모든 변수와 객체를 탐색해서 각각 어떤 객체를 참고하고 있는지 확인하고, 이후 사용되고 있는 메모리를 식별해서(Mark) 사용되지 않는 메모리는 제거(Sweep)하는 과정을 진행하게 된다.
'Java > Java' 카테고리의 다른 글
스레드의 동기화 (0) | 2023.03.12 |
---|---|
스레드 (Thread) (0) | 2023.03.12 |
스트림의 메서드 (2) | 2023.03.11 |
스트림 (Stream) (0) | 2023.03.10 |
파일 입출력 (InputStream, OuputStream, FileReader, FileWriter, File) (0) | 2023.03.09 |