왜 JVM 구조를 알아야할까?
기술면접 질문에서 자바 애플리케이션의 실행 과정과 JVM 구조를 묻는 것을 보면, 이는 JAVA 개발자가 알아야할 필수 지식이다. JVM 구조를 알아야할 필요성을 먼저 인식하고 자바 애플리케이션의 실행 과정과 JVM 구조를 알아보자.
어떤 애플리케이션에서도 메모리 관리는 중요하다. 애플리케이션의 메모리관리가 효율적으로 이뤄질 수록 애플리케이션의 성능 또한 향상되기 때문이다. 메모리 관리 방법에는 수동적인 메모리 관리법, Garbage collector를 이용한 메모리 관리법 두가지가 있다.
1. 수동적인 메모리 관리법
메모리 관리를 개발자가 직접 하는 방법이다. 메모리 할당부터 해제까지 개발자가 명시적으로 호출해줘야한다. C++과 같은 경우, new와 delete를 이용해서 메모리를 이용, 사용 해제한다. 이러한 방법은 개발자가 메모리들을 세심하게 관리할 수 있기 때문에 성능 최적화가 좋다는 장점이 있지만, 개발자의 일 부담이 커지는 단점이 있다. 또한 제대로 메모리를 해제 해주지 않으면, 더이상 할당 할 수 있는 메모리가 없는 상태가 발생한다(Memory leak).
2. Garbage Collector 등장!
이에 Garbage Collector가 등장하여 개발자가 메모리 관리에 큰 신경을 안써도 되는 상황이 온다. Garbage Collector은 현재 사용되지 않는 메모리를 찾아 메모리 할당을 해제한다. 이제 그림2와 같이 개발자들의 천국이 온걸까?!
But..
그렇다면 이젠 개발자에겐 메모리 관리 업무를 안해도 될까? 내맘대로 어디서든지 할당해도될까? 답은 No다. Garbage Collector가 있더라도 Memory leak은 일어날 수 있다. d아래와 같은 상황을 가정하자.
// Java Program to illustrate memory leaks
import java.util.Vector;
public class MemoryLeaksDemo
{
public static void main(String[] args)
{
Vector v = new Vector(214444);
Vector v1 = new Vector(214744444);
Vector v2 = new Vector(214444);
System.out.println("Memory Leaks");
}
}
위 코드는 결과로 메모리 Leak이 발생된다. 분명 GC(Garbage Collector)가 있는데 메모리 관리는 왜 안될까? 메모리 관련 오류가 있을 때 개발자가 이를 해결하려면 메모리 구조에 대해 알아야한다.
Or..
혹은 개발하던 애플리케이션이 잘 실행됬는데 어느날 애플리케이션 라이브러리 업데이트 후 아래와 같은 오류가 뜬다고 가정하자. 이때 개발자가 개발한 애플리케이션은 코드가 변경되지 않았다!
(출처: JVM Internal (naver.com))
Exception in thread "main" java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V
at com.nhn.service.UserService.add(UserService.java:14)
at com.nhn.service.UserService.main(UserService.java:19)
참고로 라이브러리는 아래와 같이 업데이트 됐다. 무슨 문제가 있었을까?
// UserAdmin.java - 업데이트된 소스코드
…
public User addUser(String userName) {
User user = new User(userName);
User prevUser = userMap.put(userName, user);
return prevUser;
}
// UserAdmin.java - 원래 소스코드
…
public void addUser(String userName) {
User user = new User(userName);
userMap.put(userName, user);
}
에러 메세지를 확인해서 오류를 찾아보자.
Msg: Exception in thread "main" java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V
메서드를 찾을 수 없다고 한다. NoSuchMethodError! 즉 'com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V' 메서드를 찾으려고 했는데 없다는 뜻이다! 맨뒤 V는 반환값이 없다는 뜻이다. 이는 라이브러리 코드와 컴파일된 실행 코드가 달라서 생기는 오류다. 컴파일 되면 해결이 된다. 여기서 생각해볼 것은 과연 개발자가 만날 오류가 이러한 것만 있을까? 혹은 이러한 사소한 오류를 만나더라도 '컴파일 눌러보니 해결되네!'와 '라이브러리는 다이나믹 링크가 되기 때문에 컴파일 된 코드에 반영되지 않구나!'를 아는 개발자는 다르지 않을까?
더 나은 개발자가 되기 위해 최소한의 지식은 갖추도록 하자.
좋은 개발자는 성능 좋은 애플리케이션을 만들고, 이는 효율적인 메모리 관리로 해낼 수 있다.
효율적인 메모리 관리를 위해 자바 애플리케이션 실행 과정과 자바 메모리 구조에 대해 알아보자.
1. 자바 애플리케이션 실행과정
자바 애플리케이션은 어떻게 실행될까? 콘솔 창에서 직접 빌드 & 실행 하는 개발자분들은 직접적으로, IDE로 직접 하는 분들은 간접적으로 java 명령어를 이용하여 애플리케이션을 실행합니다. 아래 콘솔창을 보면 java와 실행을 원하는 파일의 이름을 입력하면 우린 해당 애플리케이션을 실행할 수 있습니다. 자 그럼 실행했을 때 발생하는 일들을 차근 차근 살펴보시죠.
Oracle Doc에 따르면 java로 애플리케이션을 실행하면, 우선 Java Runtime Enviroment(JRE)를 실행한다고 합니다. JRE에서는 실행시킨 클래스를 Loading하고, 해당 클래스의 main 메소드를 실행시킵니다. 우리가 모르는 용어가 많네요 JRE는 뭐고 Loading은 뭘까요? 차근차근 알아봅시다. 참고로 그림3과 다르게 앞으로 볼 자바 애플리케이션 실행 과정은 아래 코드를 바탕해서 진행될겁니다!
코드 필참!
package homo.efficio.jvm.sample;
public class Hello {
public static void main(String[] args) {
final Hello hello = new Hello();
System.out.println(hello.helloMessage());
while (true) {}
}
public String helloMessage() {
return "Hello, JVM";
}
}
Java Runtime Enviroment(JRE)란?
JRE는 우리가 (JDK를 이용하여 작성한) 자바 코드와 (JVM상에서 자바 코드의 실행에 필요한) 필수 라이브러리를 결합한다고 합니다. 그리고 결과물인 '자바코드 + 필수 라이브러리'는 JVM 인스턴스에서 작동되는데, JRE는 JVM 인스턴스 생성까지 담당한다고하네요! 어떻게 보면 JRE는 자바 코드가 JVM에서 실행되는 환경을 조성해준다고 생각할 수 있겠군요! 그럼 차근차근 JDK와 JVM이 뭔지 확인해보죠!
참조1
JRE는 (JDK를 사용하여 작성된) Java 코드를 / (JVM에서 자바 코드의 실행에 필요한)필수 라이브러리와 결합한 후 / 결과 프로그램을 실행하는 JVM의 인스턴스를 Create합니다. 출처: IBM
Java Development Kit(JDK)이란?
JDK는 말 그대로 자바 개발 도구입니다. 자바 개발에 필요한 환경을 제공해주죠. 자바 개발엔 뭐가 필요할까요? 컴파일도 해야하고 실제 실행도 해야하고 디버깅도 해야합니다. 그럼 컴파일러, 실행기, 디버거 등 모두 JDK에 들어있어야겠네요? 맞습니다. JDK는 JRE와 JVM을 포함하는 개념이라고 보면 됩니다. 이러한 추론의 검증은 위키피디아에서 확인할 수 있습니다.
참조2
JDK provides software for working with Java applications. Examples of included software are the virtual machine, a compiler, performance monitoring tools, a debugger, and other utilities that Oracle considers useful for a Java programmer. 출처: 위키피디아
Java Virtual Machine(JVM)이란?
마지막으로 JVM은 무엇일까요? Java Virtual Machine은 컴퓨터가 자바 바이트 코드로 컴파일 된 프로그램을 실행실 수 있게 하는 가상환경입니다. 자바 바이트 코드로 컴파일된 프로그램이기 때문에 자바 뿐 아니라 다른 언어로 작성됬어도 결과만 자바 바이트 코드면 어떤 프로그램이든 실행 가능하죠. JVM에는 메모리 영역으로 기능하는 요소들과 이를 실행 & 관리하는 요소들로 이뤄져있습니다. 메모리 영역 기능 요소에는 스레드들이 공유하는 힙 공간과 메서드 영역, 스레드들이 각자 독립적으로 운영하는 JVM Language 스택, PC레지스터, 네이티브 메서드 스택이 있습니다. 해당 요소들을 실행 및 관리하는 요소에는 Execution Engine(Byte code Interpreter, Just In Time 컴파일러)와 Garbage Collector가 있습니다.
참조3
A Java virtual machine (JVM) is a virtual machine that enables a computer to run Java programs as well as programs written in other languages that are also compiled to Java bytecode. 출처: 위키피디아
우린 Java Development Kit / Java Runtime Enviroment / Java Virtual Machine 세가지를 알아봤습니다!
정리해보죠!
*요약
- JDK: 자바 프로그램 개발에 필요한 도구 및 환경을 제공해줍니다. JDK를 이용해서 바이트 코드(.class)를 만듭니다.
- JRE: 자바 바이트 코드가 JVM에서 실행될 수 있는 환경을 만들어줍니다. JRE를 이용하여 바이트 코드를 실행합니다.
- 구체적으로, JDK를 이용하여 작성한 자바코드와 이를 JVM에서 실행하기 위해 필요한 필수 라이브러리를 결합합니다. 이후 프로그램 실행에 필요한 JVM 인스턴스까지 만듭니다.
- JVM: 자바 바이트 코드로 컴파일 된 프로그램을 실행할 수 있는 가상환경입니다. 자바 바이트 코드의 실질적인 실행을 담당합니다. (메모리 할당 및 회수, 시스템 명령 호출 등)
JRE 실행에 의한 JVM 인스턴스 생성
가보자고! java를 실행한 후 jre가 실행됩니다. JRE 내에선 무슨 일이 일어날까요? 아래와 같은 두가지 일어난다고 했죠?
- JDK를 이용하여 작성한 자바코드와 이를 JVM에서 실행하기 위해 필요한 필수 라이브러리를 결합
- 프로그램 실행에 필요한 JVM 인스턴스 생성
JVM 인스턴스가 생성되면, JVM 당 하나씩 존재하는 힙 영역과 메서드 영역이 생성됩니다. Java language stack, PC register, native method stack은 어딧냐구요? 스레드가 아직 생성되지 않았기 때문에 우선 이것부터 생성됩니다. 그들은 스레드 단위로 존재하기 때문에 스레드가 우선되야하죠. 근데 힙과 메서드가 뭐냐구요? 알아보죠!
Heap 영역이란?
힙 영역은 Java object와 JRE 클래스의 런타임내 동적 할당(인스턴스화)를 위한 공간입니다. 모든 클래스와 배열들의 실질적인 객체는 힙 영역에 저장되고, 이에 대한 참조는 Java language stack 영역에 존재하게되죠. 힙 내에 있는 모든 객체는 명시적으로 회수할 수 없습니다. 이들은 추후에 설명할 Garbage collector에 의해서 회수되죠. Docs.oracle에 의하면 힙이 가득 차게 되면 메모리가 회수된다고 합니다.
우리는 JVM을 실행하기만 했고 아무 클래스도 인스턴스화 시키지 않았기 때문에 현 시점에선 Heap이 비어있습니다.[그림6] 참조.
참조4
Heap space is used for the dynamic memory allocation of Java objects and JRE classes at runtime. 출처: Baeldung
참조5
Java objects reside in an area called the heap. The heap is created when the JVM starts up and may increase or decrease in size while the application runs. When the heap becomes full, garbage is collected. During the garbage collection objects that are no longer used are cleared, thus making space for new objects. 출처: Docs.oracle
참조6
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated. 출처: Java.spec
Method 영역
메소드 영역은 컴파일된 코드(바이트 코드)의 저장 공간입니다. 해당 공간에는 클래스당 하나씩 존재하는 Run-time constant pool, Field and method data, code for method and constructors가 있습니다. .class에 존재하는 바이트 코드가 메소드 영역에 맞게 변형되서 올라간다고 보면 됩니다. 메소드 영역 또한 JVM implementation에 따라 Heap 영역으로 간주되어 GC의 대상이 될 수 있다고 합니다(출처: Java.spec)
엄밀하게 얘기하면, Run-time constant table는 클래스 정보가 메소드 영역에 저장될 때 (class에 존재하는 Constant table을 바탕해서) 만들어집니다.
참조7
The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods used in class and interface initialization and in instance initialization. 출처: Java.spec
메소드 영역 역시 비어있습니다. 현재는 JVM만 실행되고 클래스 정보를 읽은 적이 없기 때문입니다. [그림6] 참조. (런타임 상수 풀은 1-2 스크롤 정도 후에 설명됩니다 조금만 기다려주세요!)
다시 이전에 진행하던 애플리케이션 진행 과정으로 돌아가볼까요?
- 우린 JRE를 실행하여 JVM 인스턴스를 만들었습니다.
- 그 결과 JVM당 하나씩 존재하는 Heap, Method 영역이 생성됬죠.
- 클래스가 컴파일된 바이트코드가 저장될 Method 영역이 생성됬으니 이제 해당 영역에 클래스 정보를 올려야겠죠?
- 이 과정은 Loading이라고 불립니다.
- 그럼 Load는 누가할까요? Class Loader가합니다.
- Class Loader는 누가 만들까요? Class Loader는 누구고 어떤 방식으로 Load할까요? Load는 무엇일까요?
- 근데 우리가 추론한게 맞을까요? 정말로 이렇게 일어날까요?
아래는 실제 JVM이 실행된 후에 일어나는 일입니다. 추론한게 얼추 맞는군요.
근데 모르는 단어가 꽤 많습니다. Creating initial class, class loader, link, initialize etc..
너무 깊지도 않고 얕지도 않게 알아봅시다:)
참조8
The Java Virtual Machine starts up by creating an initial class or interface using the bootstrap class loader (§5.3.1) or a user-defined class loader (§5.3.2). The Java Virtual Machine then links the initial class or interface, initializes it, and invokes the public static method void main(String[]). 출처: Java.spec
Load란?
Particular name을 가지고 클래스 혹은 인터페이스가 컴파일된 binary representation(바이트 코드)를 Find하고 바이트 코드로부터 클래스 혹은 인터페이스를 Create하는 과정입니다. Load는 Find, Create 두가지를 하게 되죠.
참조9
Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation. 출처: Java.spec
Creation이란?
클래스 혹은 인터페이스의 Creation은 JVM의 메소드 영역에 Construction 하는 것으로 구성(consist)됩니다. 이는 (해당 클래스 혹은 인터페이스를 Run-time constant pool을 통해 참조하는) 다른 클래스 혹은 인터페이스에 의해서 Trigger되기도 하죠.
참조10
Creation of a class or interface C denoted by the name N consists of the construction in the method area of the Java Virtual Machine (§2.5.4) of an implementation-specific internal representation of C. Class or interface creation is triggered by another class or interface D, which references C through its run-time constant pool. Class or interface creation may also be triggered by D invoking methods in certain Java SE Platform class libraries (§2.12) such as reflection. 출처: Java.spec
오케이! Load를 할때 클래스 혹은 인터페이스를 Find하고, Creation 즉 찾은 내용을 JVM 메소드 영역에 Construct하는군요! 그렇다면 Load와 Creation을 한다면 JVM은 다음과 같이 될겁니다. 이때 런타임 상수 풀도 생성되는데, 런타임 상수 풀은 Load에 대해 알아본뒤 학습하죠!
정확히 어떻게 Find와 Creation의 과정을 알려면 너무 깊게 들어가니 지금은 지나가죠! 우린 큰 그림을 보려하는 겁니다! Load를 누가하는지는 큰 그림에 속합니다 ㅎㅎ 왜냐하면 참조8에 의하면 initial class의 creating은 bootstrap class loader 혹은 user-defined class loader에 의해 행해지기 때문이죠!
Class loader란?
Class loader는 말그대로 클래스 정보를 메서드 영역에 Load해주는 역할을 합니다. 애플리케이션 실행 중에 클래스에 대한 요청이 있으면 우선 JVM은 해당 클래스가 Load 됐는지 찾아봅니다. Load된 적 없다면, Class Loader가 클래스 파일을 찾고 메서드에 Load하게 되죠. 이때 Load도 한번에 뚝딱 되는게 아닙니다. 3가지의 Class Loader가 상호작용하며 이뤄집니다.
Class Loader 작동 과정
- ClassLoaderRunner가 Application Class Loader에게 'Internal' 클래스(클래스 이름일 뿐입니다) 로드 요청합니다.
- Application Class Loader가 Platform Class Loader(java se9부터는 Extension CL -> Platform CL로 바뀌었습니다)에게 loadClass(internal)을 delegate합니다.
- Platform Class Loader가 Bootstrap ClassLoader에게 loadClass를 delegate합니다.
- Bootstrap Class Loader가 rt.jar 경로에서 클래스가 있는지 확인합니다. 있으면 Load class하고, 없으면 Platform Class Loader에게 요청합니다.
- Extenstion Class Loader가 ext 경로에 대해 4번을 합니다
- Application Class Loader가 class path 경로에 대해 4번을 하되, 못찾으면 Exception을 일으킵니다.
Class Loader의 3가지 작동 원칙
생각보다 복잡한 과정으로 Class Loader가 작동되죠. 이는 Class Loader의 3가지 작동 원리 때문입니다.
- 위임 원칙(Delegation principle): 클래스 로더는 부모 클래스에게 클래스 로딩을 먼저 요구합니다. 자식 클래스는 부모 클래스가 클래스를 찾거나 로드할 수 없을 때 자신의 일을 합니다.
- 가시범위 원칙(Visibility principle): 자식 클래스로더는 부모 클래스 로더가 로드한 클래스들을 모두 볼 수 있습니다. 하지만 반대는 불가능합니다.
- 이는 당연합니다. 애플리케이션 클래스 로더가 만약, 부트스트랩 클래스 로더가 로딩한 String 클래스를 볼 수 없다면 이를 쓸 수도 없을겁니다.
- 유일성 원칙(Uniqueness principle: 특정 클래스는 단 한번만 로드될 수 있습니다. 이는 위임 원칙으로 구현됩니다. 해당 원칙을 이용하여 자식 클래스 로더는 부모 클래스 로더가 로드한걸 다시 로드 안하는걸 보장합니다.
참조11
Java ClassLoader is based on three principles: Delegation, Visibility, and Uniqueness.
Delegation principle: It forwards the request for class loading to parent class loader. It only loads the class if the parent does not find or load the class.
Visibility principle: It allows child class loader to see all the classes loaded by parent ClassLoader. But the parent class loader cannot see classes loaded by the child class loader.
Uniqueness principle: It allows to load a class once. It is achieved by delegation principle. It ensures that child ClassLoader doesn't reload the class, which is already loaded by the parent. 출처: JavaPoint
자 이제 Load가 뭔지, 어떻게 무엇에 의해 일어나는지 알아봤습니다. 이전에 클래스의 Load & Creation이 일어나면 메소드 영역 내 해당 클래스의 Run-time constant pool이 생성된다고 말했습니다. 이게 정말인지, 그렇다면 Run-time constant pool은 무엇인지 알아볼까요?
Run-time Constant Pool이란?
런타임 상수 풀은 클래스 및 인터페이스 단위로 존재하며, .class 파일 내 상수 풀의 런타임 표현입니다. 이는 컴파일에서 알 수 있는 숫자 리터럴 값부터 런타임에서 해설될 수 있는 메소드와 필드 참조로 구성됩니다.
런타임 상수 풀은 전통적인 프로그래밍 언어의 참조 테이블과 비슷한 역할을 한다고 보면 됩니다
참조 12
A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file (§4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time. The run-time constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table. 출처: Java.spec
로딩 이후에 일어나는 과정에 대해 알아볼까요! 참조8을 다시보면, 로딩 이후에 해당 클래스의 링크와 초기화가 일어납니다. 링크와 초기화를 간단하게 알아보도록하죠!
링크란?
특정 클래스 혹은 인터페이스를 Link한다는 것은 그 class or interface or direct superclass or direct superinterface or its element type(배열 타입일 경우)를 Verification(확인), Preparation(준비), Resolution(해석)하는 것을 의미합니다. 여기서 Verification과 Preparation는 연속적으로 일어나야하지만 Resolution은 꼭 바로 안일어나도 됩니다. Resolution은 링크 이후 초기화 과정 다음에 나타날 수도 있습니다. 이는 JVM Implementation에 따라 달라집니다.
참조12
Linking a class or interface involves verifying and preparing that class or interface, its direct superclass, its direct superinterfaces, and its element type (if it is an array type), if necessary. Linking also involves resolution of symbolic references in the class or interface, though not necessarily at the same time as the class or interface is verified and prepared. 출처: Java.spec
Verification(확인)
Verification은 클래스의 인터페이스의 바이너리 표현이 구조적으로 옳은지 확인하는 과정입니다. 이 과정은 다른 클래스나 인터페이스의 로딩을 유발할수 있지만, 확인이나 준비를 필수적으로 유발하지는 않습니다.
확인 과정에서 부모 클래스의 로딩을 유발할 수 있습니다. Hello 클래스의 부모 클래스인 Object가 로딩되겠군요!
Preparation(준비)
Preparation은 클래스와 인터페이스의 정적 필드를 생성하고 기본 값으로 초기화합니다. 구체적인 값으로의 초기화는 initialization에서 일어납니다.
Resolution(해석)
Resolution은 Run-time constant pool의 심볼릭 참조를 구체적인 값으로 동적으로 결정하는 과정입니다.
Resolution 과정에서는 해당 클래스 혹은 인터페이스의 Run-time Constant Pool의 심볼릭 참조가 가리키는 실체를 결정해야합니다. Object. <init> A 구문으로 자세히 살펴보도록 하죠. Object는 클래스고 <init>는 생성자를 나타냅니다. A는 해석될 영역이라고 볼 수 있죠. JVM은 Object의 생성자를 찾아야한다는 것을 안 채로 Object가 로딩 되있나 봅니다. 이전 Verification(확인) 과정 에서 Object의 바이트 코드가 Method 영역에 로드가 되있었죠! 그럼 JVM은 Object의 생성자 코드가 있는 곳으로 가서 그 주소를 가져와 A에다 동적으로 해석합니다.
사진만 봐도 뭔가 Link가 일어난 것 같지 않나요?:)
참조13
Verification(확인)
Verification (§4.10) ensures that the binary representation of a class or interface is structurally correct (§4.9). Verification may cause additional classes and interfaces to be loaded (§5.3) but need not cause them to be verified or prepared. 출처: Java.spec
Preparation(준비)
Preparation involves creating the static fields for a class or interface and initializing such fields to their default values (§2.3, §2.4). This does not require the execution of any Java Virtual Machine code; explicit initializers for static fields are executed as part of initialization (§5.5), not preparation. 출처: Java.spec
Resolution(해석)
Resolution is the process of dynamically determining one or more concrete values from a symbolic reference in the run-time constant pool. Initially, all symbolic references in the run-time constant pool are unresolved. 출처: Java.spec
초기화란?
초기화는 (클래스 혹은 인터페이스의 바이트 코드 내) <clinit>으로 이름 지어진 초기화 메서드를 실행하는 행위입니다. 초기화에는 인스턴스 초기화(생성자를 이용한 초기화)와 정적 초기화(클래스 정적 변수 초기화) 두개가 있습니다. 여기서 말하는 초기화는 정적 초기화입니다. 링크 내 Preparation 과정에서 생성된 클래스 혹은 인터페이스의 정적 필드는 초기화 과정에서 구체적인 값으로 초기화 됩니다.
참조14
Initialization of a class or interface consists of executing the class or interface initialization method <clinit> 출처: Java.spec
드디어 Initial 클래스를 Load, Link, Initialize 했습니다! 마침내 Main 메서드를 실행 할 수 있겠군요! 진짜로 프로그램을 실행하는 첫 흐름을 만든다고 볼 수 있죠. 그럼 프로그램 실행 흐름 단위가 필요하겠죠? 프로그램 실행의 최소 단위는 Thread입니다.
Main Thread 생성
JVM은 메인 메소드를 호출하기 위한 Main 스레드를 생성합니다. 그리고 Thread는 각각 독립적인 Java language stack, native method stack, pc register가 있죠.
저희가 초기에 작성한 '코드 필참!'에는 Main 메서드 안에 new Hello가 나옵니다. 이 코드의 실행 과정을 살펴보죠.
- 우린 Hello 클래스의 생성자를 가지고 Heap 공간에 메모리를 동적할당할겁니다.
- 해당 힙의 참조를 JVM Stack에 저장할 것이죠.
PC Register는 현재 실행중인 JVM 명령어의 위치를 저장하죠.
자바 언어가 아닌 다른 언어로 작성된 메서드를 사용할 땐 Native Method을 쓰지만 현재는 사용이 안됩니다.
해당 과정을 각 메모리 구조의 특성과 함께 자세히 살펴볼까요?
PC Register
PC Register는 JVM 명령이 실행되는 주소를 저장합니다.(메소드가 네이티브가 아닐 경우.) 메소드가 네이티브 언어일 경우, PC Register 내 값은 undefined가 됩니다. Java는 Muti thread 환경이기 때문에 스레드마다 각자의 실행 중인 주소 위치가 있기 때문에 PC Register는 스레드 마다 하나씩 존재(per-Thead)합니다.
참조15
If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. 출처: Java.spec
JVM Stack
JVM Stack은 LIFO(Last In First Out)으로 실행되는 자로구조로, frame을 저장합니다.
참조16
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. 출처: Java.spec
Frame
JVM 스택에 쌓이는 정보의 단위가 Frame이다. Frame은 데이터와 부분 결과의 저장, 동적 Linking, 메소드 결과의 return, 예외 dispatch에 사용된다. 새 프레임은 메소드가 호출되면 생성되고, 메소드 호출이 끝나면 파괴된다. 프레임은 JVM 스택 위에 올라가며 각 프레임은 local variables 배역, operand stack, 현재 메소드의 클래스의 run-time constant pool의 참조를 가진다.
- local variables: 프레임 내 지역 변수가 배열 요소로 저장됨.
- operand stack: 값을 가져오고 넘겨주는 거의 모든 과정에 활용되는 스택
참조17
A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions.
A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes, whether that completion is normal or abrupt (it throws an uncaught exception). Frames are allocated from the Java Virtual Machine stack (§2.5.2) of the thread creating the frame. Each frame has its own array of local variables (§2.6.1), its own operand stack (§2.6.2), and a reference to the run-time constant pool (§2.5.5) of the class of the current method. 출처: Java.spec
Native Method Stack
네이티브 메소드 스택은 네이티브 메소드를 지원하는 "C stack"이라고 불리는 전통적인 스택입니다. Native Method Stack은 JVM의 필수 요소는 아닙니다. native method를 Load 못하거나, 전통 스택에 의지하지 않는 방식으로 Implement 됬으면
출처 18
An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language).
Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created. 출처: Java.spec
런타임 데이터 영역 중 Per-thread 영역을 알아봤습니다. 그럼 실제로 new Hello 시 일어나는 일을 알아보죠!
1. new 명령어는 인자로 들어온 클래스의 인스턴스를 위한 공간을 힙 영역에 할당하고, 영역에 대한 참조를 오퍼랜드 스택에 쌓습니다.
2. 생성자나 슈퍼 클래스의 생성자가 호출되면 JVM Stack에 프레임 새로 생겨납니다. 생성자는 Method이기 때문이죠.
*근데 생성자는 어디에 있는지 어떻게 알까요? 이는 Hello 클래스의 런타임 상수 풀에 저장돼있습니다. 해당 클래스의 링크 내 Resolution(해석) 과정에서 Run-time constant 테이블의 심볼릭 참조를 동적으로 연결했었습니다. 이때 생성자 메소드와 이의 코드가 연결 됬던거죠.
3. Hello를 생성하려면 Hello의 Super 클래스인 Object 클래스를 생성해야합니다. Object 클래스의 참조는 Hello 클래스의 Run-time 상수 풀이 가지고 있죠. Object 프레임을 생성해봅시다!
4. 오브젝트 인스턴스가 생겨나면 Object 생성자는 자신의 일을 다했습니다. 그렇다면 생성자 메소드는 Return 값을 자신을 호출한 프레임의 오퍼랜드 스택에 Push하면서 없어집니다.
5. 이와 같은 방식을 쭈욱 따라오다보면, 처음 우리가 목표했던 '코드 필참!'의 실행 과정을 아래 그림과 같이 표현할 수 있습니다. System, PrintStream 클래스를 사용하기 위해 해당 클래스를 로드, 링크, 초기화를 해야합니다. 이후 메세지 메소드 호출을 하기위해 프레임을 생성하고 "Hello, JVM" 문자열을 Heap에 생성하게되죠!
자 이렇게 우리는 자바 애플리케이션의 실행 과정과 JVM의 구조에 대해 알아봤습니다.
조금 많이 알아본 감이 있지만 제 개인적인 시간으론 5일(3시간씩) 정도를 소요했네요. 5일 소요해서 이정도 까지 이해했다면 나쁘지 않은 시간 효율이었던 것 같습니다.
우리는 이제 Hello h = new Hello()를 하면 어떤 일이 일어나는지 JVM 스택 수준 까지 얘기할 수 있습니다! 처음 아무것도 모른 상태였을때를 생각하면 얼마나 큰 발걸음이었나요 ㅎㅎ
긴 글 읽어주셔서 감사합니다:)
'Computer launguage > Java' 카테고리의 다른 글
자바 NIO (1) 채널, 버퍼의 동작 과정을 간단한 채팅 서버/클라 애플리케이션 구현을 통해 리서치 (0) | 2024.09.01 |
---|---|
제한된 메모리 크기 환경에서의 LinkedList와 HashMap 튜닝 방법 (0) | 2022.07.03 |
[Deep dive] 부동 소수점, 고정 소수점 표현 방법? 연산 속도? 오차? 돈 계산? (0) | 2022.03.26 |
[Deep Dive] Garbage Collector(GC) 구조? 동작 과정? SE7, 8 차이? (0) | 2022.03.11 |
객체지향 프로그래밍? 장단점? 4대 원칙(추상화, 캡슐화, 상속, 다형성)이란? (0) | 2022.02.25 |