참조 타입
프로그램이 하는 일은 결국 데이터를 처리하는 일입니다. 그렇기 때문에 우리는 자바를 배우기 앞서 자바의 데이터 타입에 대해 충분히 이해하고 넘어가야 할 필요가 있습니다. 기본 타입에 대해서는 다들 어느정도 많이 알 것입니다. 오늘은 참조 타입에 대해서 자세히 알아보도록 하겠습니다!
자바의 데이터 타입으로 기본 타입(Primitive Type)과 참조 타입(Reference Type)으로 분류됩니다.
기본 타입의 경우 선언된 변수의 값을 변수 안에 저장하는 데에 반해, 참조 타입으로 선언된 변수는 그의 값이 아닌 메모리의 번지를 값으로 가집니다. 즉, 참조 타입은 번지를 통해 객체를 참조하는 데이터를 의미합니다.
아래의 코드를 살펴봅시다!
int birth = 2000406;
double weight = 3.5;
String name = "심두리";
String party = "말티즈";
int와 double 타입으로 선언된 데이터들은 데이터의 값 자체를 가지고 있습니다. 반면 String 타입으로 선언된 데이터들은 메모리 주소를 갖고 있는 것입니다. 기본 타입과 참조 타입의 데이터들은 같은 공간에 쌓이게 될까요? 정답은 아닙니다! 기본 타입은 자바의 메모리 영역 중 스택 영역에, 참조 타입은 자바의 메모리 영역 중 힙 영역에 생성됩니다. 그림으로 표현하면 아래와 같겠죠.
다시 말해, 데이터가 선언되면 스택 영역에 쌓이지만, 참조 타입의 실제 값들은 힙 영역에 쌓인다는 것을 한 눈에 알 수 있겠죠?
그렇다면 스택 영역과 힙 영역이 대체 무엇일까요?
JVM
우리의 자바 프로그램은 운영체제에서 바로 실행할 수 없습니다. 왜냐하면 자바 프로그램은 완전한 기계어가 아닌, 중간 단계의 바이트 코드이기 때문에 이것을 해석하고 실행할 수 있는 중간다리가 필요합니다. 그것이 바로 JVM, Java Virtual Machine입니다. JVM은 OS 위에서 실행되는 가상머신이며, Java와 OS 간의 중개자 역할을 합니다. JVM이 있기 때문에 어떤 OS 환경에 있더라도 우리가 자바 프로그램을 실행할 수 있는 것입니다. 따라서 자바의 메모리 영역은 JVM이 OS에게 할당 받은 메모리 영역이라고 할 수 있겠습니다.
바이트 코드는 모든 JVM에서 동일한 실행 결과를 보장하지만, JVM은 운영체제에게 종속적입니다. 이 말은 JVM을 운영체제에 맞게 설치되어야 한다는 것을 의미합니다. OS 환경에 알맞게 JVM을 설치하면 하나의 바이트 코드에서 다른 기계어로 번역될 수 있습니다.
JVM은 자바의 큰 장점이지만, 한 번의 컴파일로 실행 가능한 기계어가 만들어지는 것이 아닌 기계어로 번역되고 실행되기 때문에 C와 C++의 컴파일보다 속도가 느립니다. 그 둘을 비교하는 대략적인 그림은 아래와 같겠습니다.
C는 소스가 들어오면 컴파일러를 통해서 기계어가 됩니다. 반면에 ava는 소스가 들어오면 컴파일러를 통해서 바이트 코드가 되고 그 이후에 JVM에서 기계어로 변환이 됩니다.
우리가 자바 프로그램을 실행하기 위해서는 JVM이 필요하다고 언급하였는데, JVM을 어떻게 설치해야 할까요? 바로 JDK와 JRE를 깔면 자동으로 JVM이 설치가 됩니다. JDK(Java Development Kit)와 JRE(Java Runtime Environment)은 Java SE(Standard Edition)의 구현체입니다. JDK는 프로그램 개발에 필요한 자바 가상 기계(JVM), 라이브러리 API, 컴파일러 등의 개발 도구가 포함되어 있고, JRE에는 프로그램 실행에 필요한 자바 가상 기계(JVM), 라이브러리 API만이 포함되어 있습니다. 우리가 프로그램을 만드는 것이 아니라 실행만 필요하다면 JRE만 설치해도 됩니다.
-
JRE = JVM + 표준 클래스 라이브러리
- JDK = JRE + 개발에 필요한 도구
그러면 위의 컴파일 과정을 조금 변경해서 그릴 수 있겠죠?
JVM의 구조는 어떻게 돼있을까요? 컴파일러로부터 받은 바이트 파일(*.class)들은 어떤 과정을 겪게 되는 걸까요?
좀 더 자세한 JVM의 구조를 보면서 함께 살펴봅시다!
code.class 라는 바이트 파일은 이미 우리의 소스코드(*.java)가 JDK의 컴파일러에서 바이트 파일로 바뀌어 넘어온 것입니다. 바이트 파일은 JVM의 Class Loader에게 먼저 갑니다.
-
Class Loader
- 클래스 로딩 과정을 수행하는 주체입니다.
- 확장자가 .class인 파일의 위치를 찾아 클래스를 메모리에 로드하고 사용할 수 있게 만들어줍니다.
- 링크를 통해 Runtime Data Area에 배치하는 작업을 수행하는 모듈입니다.
- 런타임 시 동적으로 클래스를 로드합니다.
-
Execution Engine
- Class Loader를 통해 JVM 내의 런타임 데이터 영역에 배치된 바이트 코드를 실행합니다.
- 이 경우, 자바 바이트 코드를 명령어 단위로 읽어서 실행하는 역할을 합니다.
-
Garbage Collector
- JVM은 GC를 통해 메모리 관리 기능을 자동으로 수행합니다.
- 애플리케이션이 생성한 객체의 생존 여부를 판단해 더이상 사용되지 않는 객체를 해제하는 방식으로 자동 관리합니다.
-
Runtime Data Area
- JVM의 메모리 영역입니다.
- JVM이 운영체제 위에서 실행되며 할당 받는 메모리 영역입니다.
- Class Loader에서 준비한 데이터들을 보관하는 저장소이기도 하지요.
우리가 궁금해하던 JVM의 메모리 영역이 나왔네요. JVM의 메모리 영역인 Runtime Data Area의 구조를 더 자세히 볼까요?
이 그림에는 나와있지 않지만, JVM의 구조 그림에서는 PC Register와 Native Method Stack이 있었죠? 걔네들의 역할부터 하나씩 정리해봅시다. 사실 저는 이해가 잘 가지 않아 여기저기서 찾아봤습니다. 참고한 사이트는 맨 아래에 달아두었으니 자세한 설명이 필요하다면 참고하세요!
-
PC Register
먼저 Register라는 것을 알아볼까요? 프로그램의 실행은 사실 CPU에서 명령어(Instruction)을 수행하는 과정으로 이루어집니다. 이때 CPU는 이런 연산을 수행하는 동안 필요한 정보를 레지스터라고 하는 CPU 내의 기억장치를 사용하게 됩니다. 그러니까, A와 B라는 데이터, 즉 비연산값인 Operand가 있고 이를 더하라는 연산, Instruction이 있다고 봅시다. A와 B, 그리고 더하라는 연산이 순차적으로 입력이 되는데, 이때 A를 받고 B를 받는 동안 이 값을 CPU가 기억해두어야 합니다. 하지만 얘네를 다 기억하기 위해서 메모리에 올리는 방법은 너무 비효율적이기 때문에 잠시 기억하는 공간이 필요합니다. 이 공간이 바로 CPU 내의 기억장치, Register입니다.
하지만 PC Register는 위의 Register와 다릅니다. 자바는 Register-base가 아닌, Stack-base로 작동기 때문입니다. 자바는 OS나 CPU의 입장에서는 하나의 프로세스이기 때문에 가상 머신, JVM의 리소스를 이용해야 합니다. 그래서 자바는 CPU에 직접 연산을 수행하도록 하는 것이 아닌, 현재 작업하는 내용을 CPU에게 연산으로 제공해야 하며, 이를 위한 버퍼 공간으로 PC Register라는 메모리 영역을 만들게 된 것입니다. JVM은 스택에서 비연산값, Operand를 뽑아 별도의 메모리 공간, PC Register에 저장하는 방식을 취합니다.
PC Register에 직접적으로 연산을 저장하는 방식이 아닌 연산의 주소 값을 저장하는 방식을 취하고 있습니다. 그래서 PC Register는 현재 실행하고 있는 부분의 주소를 가지고 있으며, PC의 값은 현재 명령이 끝난 뒤에 값을 증가시켜, 해당하는 값의 명령을 실행하게 됩니다. PC의 풀네임이 Program Counter라는 것이 명시적으로 드러나는 부분입니다.
PC Register는 스레드가 시작할 때 생성되며 스레드마다 하나씩 존재합니다. 만약에 스레드가 자바 메소드를 수행하고 있으면 JVM 명령(Instruction)의 주소를 PC Register에 저장하게 됩니다. 하지만 다른 언어의 메소드를 수행하고 있다면, undefined 상태가 됩니다. 왜냐하면 이 두 경우를 따로 처리하기 때문이죠. 이 부분이 바로 뒤에 언급하게 될 Native Method Stack 공간입니다.
-
Native Method Stack Area
이 공간은 자바 프로그램의 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램의 호출을 저장하는 영역입니다. JVM은 네이티브 방식(JNI: Java Native Interface)을 지원하기 때문에 스레드에서 네이티브 방식의 메소드가 실행되면 Native Method Stack Area에 쌓이게 됩니다. 일반적으로 메소드를 실행하는 경우 JVM 스택에 쌓이다가 해당 메소드 내부에 네이티브 방식을 사용하는 메소드가 있다면 해당 메소드는 네이티브 스택에 쌓입니다. 그리고 네이티브 메소드가 수행이 끝나면 다시 자바 스택으로 돌아오게 되는데, 네이티브 메소드가 호출한 스택 프레임으로 돌아가는 것이 아닌 새로운 스택 프레임을 하나 생성해서 여기서 다시 작업을 수행하게 되는 것입니다. 그래서 네이티브 코드로 되어 있는 함수의 호출을 자바 프로그램 내에서도 직접 수행할 수 있고 그 결과를 받아올 수도 있습니다.
-
메소드 영역
메소드 영역에는 코드에서 사용되는 클래스(~.class)들을 클래스 로더로 읽어 클래스 별로 런타임 상수풀(Runtime Constant Pool), 필드 데이터(Field Data), 메소드 데이터(Method Data), 메소드 코드(Method Code), 생성자 코드(Constructor Code) 등을 분류해서 저장합니다. 메소드 영역은 JVM이 시작할 때 생성되고 모든 스레가 공유하는 영역입니다.
-
힙 영역
힙 영역은 객체와 배열이 생성되는 영역입니다. 힙 영역에 생성된 객체좌 배열은 JVM 스택 영역의 변수나 다른 객체의 필드에서 참조합니다., 참조하는 변수나 필드가 없으면 의미없는 객체가 되어 이것을 쓰레기로 취급해 JVM은 Garbage Collector를 실행시켜 쓰레기 객체를 힙 영역에서 자동으로 제거합니다. 그래서 개발자는 객체를 제거하기 위해 별도의 코드를 작성할 필요가 없으며, 자바는 사용자에게 코드로 객체를 직접 제거시키는 방법을 제공하지 않습니다.
-
JVM 스택 영역
JVM 스택 영역은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 할당됩니다. 자바 프로그램에서 추가적인 스레드를 생성하지 않았다면 main 스레드만 존재하여 JVM 스택도 main 스레드의 것 하나뿐입니다. JVM 스택은 메소드를 호출할 때마다 프레임(Frame)을 추가(Push)하고 메소드가 종료되면 해당 프레임을 제거(Pop)하는 동작을 수행합니다. 예외 발생 시에 printStackTrace() 메소드로 보여주는 Stack Trace의 각 라인은 하나의 프레임을 표현합니다.
프레임 내부에는 로컬 변수 스택이 있습니다. 변수가 이 영역에 생성되는 시점은 초기화가 될 때, 즉 최초로 변수에 값이 저장될 때입니다. 변수는 선언된 블록 안에서만 스택에 존재하고, 블록을 벗어나면 스택에서 제거됩니다.
'개발 공부' 카테고리의 다른 글
[자바] 참조 타입 - 2 (0) | 2020.05.04 |
---|---|
[운영체제] Interrupt의 원리 (2) | 2020.05.02 |
[프로그래머스] 42897 도둑질 (0) | 2020.04.28 |
[프로그래머스] 42898 등굣길 (0) | 2020.04.28 |
[MySQL] MySQL 접속 시 오류 해결 (0) | 2019.07.03 |