본문 바로가기
자바과정/Java

JVM의 구조

by Parkej 2021. 10. 4.

# 출처 및 참고

 

 

JVM(Java Virtual Machine)

- 자바 가상 머신으로 자바 바이트 코드를 실행 할 수 있는 주체이다. 이 JVM 덕분에 CPU나 운영체제와 독립적으로 동작이 가능하다. 

 

- JVM은 크게 세가지로 나눌 수 있다. 

JVM 구조

 

# 클래스 로더 (Class Loader)

- 자바 바이트코더를 읽어들여 메모리에 적절하게 배치하는 일을 한다. (.class에서 바이트 코드를 읽고 메모리에 저장)

- 클래스 로더는 크게 3가지로 나누어 볼 수 있다.

 1. 로딩(loading) : 실제 클래스 파일에서 바이트 코드를 읽어오는 역할을 한다.

 2. 링크(linking) : 그 안에 레퍼런스를 연결한다.

 3. 초기화(initialization) : 클래스에 있는 static한 값들을 초기화 한다. (static 값들 초기화 및 변수에 할당)

 

변수 선언 부분
정적 변수 사용 부분
StaticTest 클래스에서 다른 클래스의 정적 변수를 가져옴

- 이러한 값들을 초기화하는 과정을 진행한다. 

 

클래스 로딩을 위한 JVM의 로딩 절차

1. 어떤 메소드를 호출하는 문장을 만났는데, 그 메소드를 가진 클래스 바이트코드가 아직 로딩된 적이 없다면, 곧바로 JVM은 JRE라이브러리 폴더에서 클래스를 찾는다.

2. 없으면, CLASSPATH 환경변수에 지정된 폴더에서 클래스를 찾는다.

3. 찾았으면 그 클래스 파일이 올바른지 바이트 코드를 검증한다. 

4. 올바른 바이트코드라면 메소드 영역으로 파일을 로딩한다. 

5. 클래스 변수를 만들라는 명령어가 있으면 메소드 영역에 그 변수를 준비한다.

6. 클래스 블록이 있으면 순서대로 그 블록을 실행한다. 

7. 이렇게 한번 클래스의 바이트코드가 로딩되면 JVM이 종료될때까지 유지된다. 

# 메모리 (Memory)

JVM에서 메모리는 크게 5가지 영역으로 나뉘어져 있다. 

1. 메소드 영역 : 클래스 수준의 정보(클래스 이름, 부모 클래스 이름, 메소드, 변수) 저장, 공유자원이다.

 - 클래스 이름과 풀 패키지 경로 또는 상속받은 부모 클래스

 - 현재 HelloJava 클래스에는 보이진 않지만 기본값으로 상속받은 클래스가 있다. (Object 클래스)

- 이것들은 모두 메소드 영역의 메모리에 저장이 된다. 

- 해당 영역에 있는 것들은 공유하는 자원이다. 즉, 다른 영역에서도 참조할 수 있는 정보들이다. 

 

2. 힙 영역

- 객체를 저장, 공유 자원이다.

- 메소드 영역에는 클래스 수준의 정보들을 저장한다면, 실제 인스턴스(객체)들을 저장한다.

- 명시적으로 생성한 인스턴스(객체)들 모두 힙에 저장된다.

- 동적으로 생성된 오브젝트와 배열이 저장되는 곳으로 Garbage Collection의 대상이 되는 영역이다. 

 

* 나머지 스택, PC, 네이티브 메소드 스택 이 3가지는 쓰레드에 국한된다.

- 어떤 쓰레드인지에 따라 해당 쓰레드에서만 공유하는 자원이다.

- 힙이나 메소드처럼 다른 모든 영역에 공유하는 자원들은 아니다.

 

3. 스택 영역

쓰레드마다 런타임 스택이라는 것을 만들고 그 안에 메소드 호출을 스택 프레임이라 부르는 블록으로 쌓는다. 쓰레드를 종료하면 런타임 스택도 사라진다.

* 스택 프레임 : 메서드 콜

* 아래와 같은 콜 스택이 쌓여있는 것이다. 

- 이러한 스택은 쓰레드마다 하나씩 만들어진다. 

 

이렇게 만들어진 스택에 메소드를 쌓았는데 현재 어느 위치를 실행하고 있는지를 가리키는 PC Register라는 것이 생긴다. 이것 역시 해당 스택에 국한되어 있다.

 

- 지역 변수, 파라미터 등이 생성되는 영역, 동적으로 객체를 생성하면 실제 객체는 Heap에 할당되고 해당 레퍼런스만 Stack에 저장된다. Stack은 스레드별로 독자적으로 가지게 된다. 

 

* Heap에 있는 오브젝트가 Stack에 참조 할 수 없는 경우 GC의 대상이 된다. 

 

4. PC(Program Counter) Register 영역

 - 쓰레드마다 쓰레드 내 현재 실행할 스택 프레임을 가리키는 포인터가 생성된다.

 - 현재 쓰레드가 실행되는 부분의 주소와 명령을 저장하고 있다. (CPU의 PC Register와는 다름.)

 

5. 네이티브 메소드 스택

 - 네이티브 메소드 호출할 때 사용하는 별도의 메소드 스택이다.

 - 자바외 언어로 작성된 네이티브 코드를 위한 메모리 영역.

포스팅되는 내용들은 우리가 무언가를 할 때 응용하는 것이 아니라 자바 애플리케이션을 프로파일링 할 때 주로 사용한다.

 

* 네이티브 메소드란?

 - 메서드에 네이티브라는 키워드가 붙어있고 그 구현을 자바가 아닌 C나 C++로 한 것을 얘기하는 것이다.

 - 예제 ) 쓰레드

 - 해당 내용을 JNI(Java Native Interface)라고 부른다.

 - 실제 구현된 자체를 네이티브 메소드 라이브러리라고 한다. 해당 라이브러리는 항상 JNI를 통해서 써야 한다. 

 

우리가 프로그래밍을 하면서 네이티브 코드를 사용한다면 네이티브 메소드 스택이 생길것이고 그 안에 네이티브 메소드 인터페이스를 호출하는 JNI 스택 프레임이 하나 쌓인다.

 

 

# 실행 엔진(Execution Engine)

메모리에 적재된 클래스들을 기계어로 변경해 명령어 단위로 실행하는 역할을 하게된다.

 - 명령어를 하나 하나 실행하는 인터프리터 방식과 실행 시점에 자주 쓸만한 코드들을 기계어로 변환시켜놓고 저장해서 사용하는 JIT 방식이 있다. 

 

인터프리터

 - 인터프리터는 바이트 코드를 이해할 수 있다. 

- 이렇게 창에 보이는 바이트 코드를 한줄씩 실행한다. 

- 한줄 한줄 실행하면서 네이티브 코드로 기계가 이해할 수 있게 바꾸고 실행하는 것. (한줄씩 이해하면서 실행한다고 생각하면 된다.)

 

하지만 이것을 한 줄씩 네이티브 언어로 이해한다는 것인데(바이트 코드를 네이티브 코드로 한줄씩 컴파일) 똑같은 코드가 여러번 나와도 매번 네이티브 코드로 바꾸는 것이 비효율적이라 반복적인 코드가 발생했을 때 JIT 컴파일러한테 맡기게 된다.

 

* JIT 컴파일러

 - 바이트 코드를 네이티브 코드로 컴파일 해준다.

 - 반복된 코드를 전부 찾아 미리 바꾸는 작업을 한다.

 - 인터프리터가 해당하는 라인에 걸렸을 때 인터프리팅 하는 것이 아닌 네이티브 코드로 바뀌어져 있던 것을 바로 사용하게 되는 것이다.

 - 인터프리터 효율을 높이기 위해, 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 바꿔둔다. 그 다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용한다. 

 

 

* JNI(Java Native Interface)

 - 자바 애플리케이션에서 C, C++, 어셈블리어로 작성된 함수를 사용할 수 있는 방법을 제공

 - Native 키워드를 사용한 메소드 호출

 

* 네이티브 메소드 라이브러리 

 - C, C++로 작성된 라이브러리

 

이러한 JVM 구조로 인해 프로그램 실행 속도를 향상시키는 것이다. 

 

 

** GC (가비지 컬렉터) 

 - 더 이상 참조되지 않는 객체를 모아서 정리한다.

 - GC를 크게 2가지로 분류한다면 쓰로우 풀 GC스탑더월드를 줄이는 GC가 있다.

 - 서버 운영중에 많은 객체를 생성하고 리스폰스 타임이 굉장히 중요하다. 이 경우 스탑더 월드 GC 할 때 방생하는 어떤 멈춤(pause)현상을 최소화 할 수 있는 GC를 사용하게 된다. 

 - GC가 하는 일은 실행 엔진의 일부인 것이다.

 - Heap(힙) 메모리 영역에 생성 된 객체들중에 Reachability를 잃은 객체를 탐색 후 제거하는 역할을 한다. 

 

가비지(Garbage)
 - 정리되지 않은 메모리
 - 유효하지 않은 메모리 주소

 

- 자바 프로그래밍 기초를 공부하면서 또는 코드의 흐름을 공부하면서 배웠던 내용은 arr[0]과 arr[1]에 저장되었던 값들은 새로운 arr = new String[] {"G", "C"}; 코드가 등장하면서 자동적으로 바뀐다로만 알고 있었다. 하지만 그 속내는 어떻게 돌아가는지 모르는 경우가 나였다. 

 

- 위 코드에서 String 배열이 할당되기 전에 할당한 a와 b는 어디로 갔을까? 이렇게 주소를 잃어버려서 사용할 수 없는 메모리가 정리되지 않은 메모리이다. 프로그래밍 언어에서는 Danling Object, 자바에서는 Garbage라고 부른다. 

 

더하여 앞으로 사용하지 않고 메모리를 가지고 있는 객체 역시 Garbage에 포함된다. 

- 가비지는 메모리가 부족할 때 이런 가비지들을 메모리에서 해제시켜 다른 용도로 사용 할 수 있게 해주는 프로그램을 말한다.

 

 C++과 같은 다른 언어에서는 직접 객체의 메모리들을 관리해주어야 하지만 자바는 JVM의 GC로 인해 개발에서는 편리하다. 하지만 모든 메모리 누수를 잡아주는것은 아니라고 하니 경계를 늦추지 말자 .

 

 

Stop The World

- GC 실행을 위해 JVM이 애플리케이션 실행을 멈추는 것이다. GC가 실행될 때는, GC를 실행하는 쓰레드를 제외한 모든 쓰레드들이 작업을 멈춘다. GC 작업이 완료된 후에야 중단했던 작업들이 시작된다. 

* 대개의 경우 GC 튜닝이란 이 Stop-the-world 시간을 줄이는 것을 말한다. 

 

GC의 과정

- Mark and Sweep이라고도 부른다. GC가 스택의 모든 변수 또는 Reachable 객체를 스캔하면서 각각 어떤 객체를 참조하고 있는지 찾는 과정이 Mark라고 한다. 이 과정에서 *Stop the world가 발생하게 되는 것이다. 이후 Mark 되어있지 않은 객체들을 힙에서 제거하는 과정이 Sweep이다. 

 

* Reachable : Stack에서 Heap 영역의 객체에 대해 참조 할 수 있느냐를 얘기함. 

 

Reachability

- Java의 GC는 가비지 객체를 판별하기 위해 reachability라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 reachable 없으면 unreachable로 구별하고 unreachable 객체를 가비지로 구분한다. 

 

- 기본적으로 new에 할당되는 메모리들은 모두 Strong Reference를 가지기 때문에 캐시와 같은 것을 만든다고 할 때 메모리 누수에 조심해야 한다. 캐시의 키가 원래 데이터에서 삭제된다면 캐시 내부의 키와 값은 더 이상 의미가 없는 데이터 즉, 가비지가 된다. 

그럼에도 GC는 삭제된 캐시의 키를 바로 인식하지 못한다. 이는 캐시에 넣어준 데이터가 Strong Reference로 독자적인 Reachability를 가지기 때문이다. 따라서 캐시에 데이터를 넣어 줄 때, 원래 데이터에 Weak Reference를 넣어준다면 이러한 문제를 방지 할 수 있다. Weak Reference는 new로 할당된 객체의 유혀 참조를 인위적으로 설정 할 수 있게 해주기 때문에 원래의 데이터가 삭제되면 이 객체에 Weak Reference가 갈려있는 객체들은 모두 가비지로 인식된다. 

 


해당 JVM 내용들을 정리하자면 다음과 같다.

1. 클래스 로더가 읽어들인다. 

2. 메모리에 맞게 적재(배치)한다.

3. 실행할 때 어떤 쓰레드가 만들어지면 쓰레드에 맞게 스택, PC, 네이티브 메소드 스택이 생성된다.

4. 실행 엔진이 바이트 코드를 한줄씩 실행하면서 어떤 코드는 저런 스택에다가 넣는 것도 있고 빼내어 더하는 것도 있고 등등 실행하면서 스택을 사용하게 된다. 

5. 한줄 한줄씩 읽는게 비효율적이다 보니 JIT 컴파일러를 사용한다. 

6. 메모리도 최적화 해주어야하기 때문에 남는 레퍼런스(인스턴스)를 찾아 정리해준다.

7. 메모리나 실행 엔진이 네이티브 라이브러리를 사용한다면 JNI를 통해 사용하게 된다. 

 

반응형

'자바과정 > Java' 카테고리의 다른 글

쓰레드(Thread)란  (0) 2021.10.07
Java 클래스 로더(ClassLoader)  (0) 2021.10.04
JVM과 JDK와 JRE  (0) 2021.09.23
객체 지향 프로그래밍 5원칙 (SOLID)  (0) 2021.09.09
객체 지향 프로그래밍 4대 특징 (코드 실습)  (0) 2021.09.09

댓글