안녕하세요~ 백엔드 개발을 공부 중인 대학생입니다.
이전 글에서 boxing, unboxing에 대해 알아보면서 참조 타입은 heap 영역에, 이를 가리키는 주소가 stack 영역에 저장된다고 했습니다.
https://yeong0jae.tistory.com/18
Boxing과 Unboxing으로 인한 성능 저하를 해결하는 Kotlin
안녕하세요. 이번 글에서는 java에서 boxing과 unboxing으로 인한 성능 저하에 대해 알아보고, 이를 해결하는 Kotlin의 특징에 대해서도 알아보겠습니다. boxing과 unboxing에 대해 들어보셨나요? 이를 설
yeong0jae.tistory.com
이번에는 더 구체적으로! 자바에서 변수, 객체 등이 생성될 때 메모리가 어떻게 할당되고 관리되는지에 대해 알아보겠습니다.
덧셈 코드 예제
먼저 간단한 덧셈 계산 코드를 작성해 보겠습니다.
public class Application {
public static void main(String[] args) {
int num1 = 3;
int num2 = 2;
int sum = num1 + num2;
}
}
위의 자바 프로그램은 어떻게 실행될까요?
먼저 프로그램이란 무엇인가요? 컴퓨터가 실행할 수 있는 명령어들의 집합입니다.
그리고 이 프로그램이 실행되면, 실행 중인 프로세스라고 부를 수 있습니다.
프로세스는 컴퓨터의 CPU가 실행합니다. 그리고 프로세스는 독립된 메모리 공간을 할당받습니다.
위 코드의 경우는 간단히 정리하면
- CPU는 num1라는 변수에 값 3을 저장하라는 명령을 수행하게 되고,
- num1 = 3 값은 stack이라는 메모리 공간에 저장됩니다.
num2, sum도 똑같습니다. num2 = 2, sum = 5가 되어 stack에 저장됩니다.
stack이란?
위 코드에서 num1, num2, sum이 저장되는 stack이라는 공간은 무엇일까요?
이를 알아보기 위해 JVM(자바 가상 머신) 공식 문서를 확인해 보겠습니다.
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
Chapter 2. The Structure of the Java Virtual Machine
Conditional branch: ifeq, ifne, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt if_icmpge, if_acmpeq, if_acmpne.
docs.oracle.com
아래는 2.5.2. Java Virtual Machine Stacks의 일부 내용입니다.
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.
Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.
-> 먼저 각 JVM 스레드는 자신만의 JVM 스택을 가지며, 스택은 스레드가 생성될 때 함께 생성됩니다. 그리고 스택은 프레임이라는 것을 저장합니다. 지역 변수와 연산의 중간 결과를 저장하고, 메서드 호출과 반환에서 역할을 한다고 합니다.
-> 추가로 자바 가상 머신의 스택은 프레임을 push, pop 하는 방식으로만 직접적으로 조작되며 프레임이 힙에 할당될 수도 있다고 합니다.
그렇다면 각 스레드가 실행하는 메서드 호출이나 지역 변수는 다른 스레드와 간섭하지 않으며 스레드 안전(Thread Safety)하다고 볼 수 있겠네요. 또한 자바 가상 머신의 스택은 이름 그대로 스택 자료구조처럼 push, pop 되는 LIFO(Last In First Out) 구조로 동작함을 알 수 있습니다.
다시 코드로 돌아가서 프로그램이 실행될 때의 stack 영역을 한번 떠올려볼 수 있겠네요.
위 그림과 같이 지역변수로 선언된 num1 = 3, num2 = 2, sum = 5가 차례로 stack 영역에 쌓일 것입니다.
▶ 메서드 콜과 frame의 생성
위 문서에서 JVM의 stack은 frame을 저장한다고 했는데, frame은 무엇일까요?
아래는 2.6. Frames의 일부 내용입니다.
A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions. 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.
-> frame(프레임)은 데이터를 저장하고, 동적 연결, 메서드의 반환 값, 예외 전달에도 사용됩니다. 프레임은 JVM의 stack에서 할당되며, 각 프레임에는 자체로 관리하는 지역 변수 배열, 피연산자 스택, 현재 돌아가는 클래스의 참조를 갖습니다.
Only one frame, the frame for the executing method, is active at any point in a given thread of control. This frame is referred to as the current frame, and its method is known as the current method. The class in which the current method is defined is the current class.
→ 스레드의 제어 어느 지점에서든 실행 메서드를 위한 단 하나의 프레임만 활성화되며 그 프레임을 현재 프레임이라고 하고 프레임의 메서드를 현재 메서드라고 합니다. 현재 메서드가 정의된 클래스가 현재 클래스입니다.
A frame ceases to be current if its method invokes another method or if its method completes. When a method is invoked, a new frame is created and becomes current when control transfers to the new method. On method return, the current frame passes back the result of its method invocation, if any, to the previous frame. The current frame is then discarded as the previous frame becomes the current one.
→ 해당 메서드가 다른 메서드를 호출하거나 완료되면 프레임은 더 이상 현재 상태가 아니며, 새 프레임이 생성되고 제어가 새 메서드로 전송되면 이 메서드는 현재 프레임이 됩니다. 메서드가 반환될 때 현재 프레임은 메서드 호출 결과를 이전 프레임으로 다시 전달합니다. 현재 프레임은 이전 프레임이 현재 프레임이 되면서 삭제됩니다.
저희가 코드를 짜며 흔히 말하는 “현재 실행 중인 메서드”와 같은 단어가 내포하는 의미까지도 JVM의 동작에 근거하여 정리되어 있다는 점이 놀라운 것 같네요. 🫢
(공식 문서를 보는 것이 참 중요한 것 같습니다.)
위에서 중요한 내용은 메서드 체이닝이 될 때, 스택에 프레임을 생성해 쌓고(push), 반환(pop)하는 동작입니다.
코드로 예시를 들어보겠습니다.
public class Application {
public static void main(String[] args) {
int num = 3;
num = multiplyByTwo(num);
}
private static int multiplyByTwo(int operand) {
int doubled = operand * 2;
return doubled;
}
}
main 메서드가 호출되면 stack에 frame이 생성됩니다. 그 frame에는 num = 3이 저장될 것입니다. 그리고 multiplyByTwo라는 메서드가 호출되면 새로운 프레임이 그 위에 생성됩니다.
프레임에는 operand와 doubled라는 지역 변수와 매개 변수가 저장되고, 메서드가 종료되면 doubled를 반환하며 현재 프레임은 종료됩니다. 종료되면 다시 main은 현재 프레임이 되고, 현재 메서드가 되겠죠.
그림을 그려보겠습니다.
(위 두 개의 stack은 다른 공간이 아닌 같은 stack의 변화를 나타낸 것입니다.)
multiplyByTwo가 호출되면 새로운 프레임이 생성되고, 프레임에 매개변수와 지역 변수가 저장됩니다.
그리고 작업이 완료되어 메서드가 리턴되면 프레임은 사라지고 리턴 값이 이전 프레임으로 돌아갑니다.
이제 메서드 호출 시 메모리 할당이 어떻게 이뤄지는지 어느 정도 이해가 되네요.
▶ StackOverFlowError
그렇다면, 만약 할당된 stack 메모리 공간보다 더 많이 메서드가 호출되면 어떻게 될까요?
메서드 호출 수가 정말 많은 재귀 함수라던지, 무한 루프를 도는 재귀 함수라면 그럴 수도 있겠네요.
이 때는 우리가 흔히 아는 StackOverFlow 에러가 발생하게 됩니다.
If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
→ 스레드의 계산에 허용된 것보다 더 큰 Java Virtual Machine 스택이 필요한 경우 Java Virtual Machine은 StackOverflowError를 발생시킵니다.
2.5.2. Java Virtual Machine Stacks 에도 나와있는 내용이네요.
Heap이란?
지금까지는 Stack 메모리 공간에 대해서 알아보았는데, 자바 애플리케이션을 실행할 때 JVM은 Stack 영역만을 활용할까요?
정말 중요한, 자바에서 참조 타입의 값이 저장된다고 알려진 Heap 영역도 있습니다.
아래는 2.5.3. Heap의 일부 내용입니다.
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.
→ JVM은 스레드 간 공유 가능한 heap이 있습니다. 힙은 런타임 데이터 영역이며, 모든 클래스 인스턴스와 배열에 대한 메모리가 할당됩니다.
The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated.
→ 힙은 가상 머신 시작 시 생성됩니다. 힙 저장소는 자동 저장 관리 시스템 (가비지 컬렉터 GC)에 의해 회수되고, 명시적으로 할당 해제되지 않습니다.
Heap은 다시 말해, 자바에서 객체가 저장되는 영역이라고 볼 수 있습니다.
이번에는 새로운 코드로 예시를 들어보겠습니다.
public class Application {
public static void main(String[] args) {
Car car = new Car();
race(car);
int position = car.getPosition();
}
public static void race(Car car) {
car.move();
car.move();
}
}
public class Car {
private int position = 0;
public void move() {
position++;
}
public int getPosition() {
return position;
}
}
Heap 영역에 객체가 어떻게 저장되는지 알아보기 위해 Application에서 사용할 Car 클래스를 만들었습니다.
Car는 position이라는 인스턴스 변수를 갖고, move, getPosition 메서드를 만들어주었습니다.
이제 Application의 main이 실행되면 Stack과 Heap에 어떻게 변수들이 할당될까요?
▶ Car 객체 생성
먼저 new Car(); 가 호출됩니다. 생성자도 곧 메서드이기 때문에, 스택에 프레임이 생성됩니다.
이때 Heap에 Car 타입의 객체도 함께 생성됩니다. 그리고 힙에는 Car 인스턴스가 갖는 position이라는 인스턴스 변수도 같이 저장됩니다.
생성이 끝나면, 생성자 스택 프레임이 사라지고 다시 main 스택 프레임에 돌아와서 car 지역변수는 heap에 생성되었던 Car 객체를 가리키게(주소값을 저장) 됩니다.
지금까지 과정을 그림으로 보여드리겠습니다.
main 메서드의 지역변수 car는 heap에 생성된 Car 객체를 가리키게 됩니다.
▶ race() 메서드 호출
이후 race 메서드가 호출됩니다.
race의 매개변수 car는 힙에 있는 Car 객체를 가리킵니다. 그리고 car의 move 메서드에 대한 프레임도 차례로 생성됩니다.
move 메서드의 프레임에는 자신 인스턴스를 가리키는 this가 내포되어 있는데 이를 이용해 Car 객체의 position을 증가시킵니다.
그리고 move()와 race()가 완료되면 프레임은 사라지고 main 메서드의 지역변수 position에 2 값이 저장됩니다.
여전히 지역변수 car는 heap에 있는 Car 객체를 가리키고 있습니다.
▶ Garbage Collector(GC)의 역할
위 코드에서는 Car 객체를 힙에 생성하고, 이를 가리키는 변수가 존재했습니다.
하지만 객체를 생성하고 이를 아무도 가리키지 않는다면 어떻게 될까요?
JVM 공식 문서에는 “힙 저장소는 자동 저장 관리 시스템 (가비지 컬렉터 GC)에 의해 회수”된다고 말합니다.
코드를 보겠습니다.
public class Application {
public static void main(String[] args) {
Car car = new Car();
race(car);
int position = car.getPosition();
}
public static void race(Car car) {
Car trashCar = new Car();
car.move();
car.move();
}
}
만약 race 메서드에 Car trashCar = new Car();라는 코드가 있다면 어떻게 될까요?
race 메서드가 실행되면, 프레임이 스택에 생성되고 move를 두 번 수행한 뒤 반환됩니다.
하지만 race가 반환되면 trashCar 지역변수도 사라지고, 이때 Heap에 생성한 Car 객체를 가리키는 변수는 애플리케이션에 존재하지 않게 됩니다.
이때 아무도 가리키지 않는 Car 객체를 쓰레기 객체라고 할 수 있으며, 이런 쓰레기 객체들에 의해 Heap이 낭비되지 않기 위해 JVM은 Garbage Collector라는 힙 저장소는 자동 저장 관리 시스템이 있습니다.
지금까지 자바 애플리케이션이 실행될 때, JVM의 메모리 관리 방식을 알아보았습니다.
중요한 것은, 객체 즉 heap에 저장되는 데이터는 어느 스레드든, 어느 메서드 프레임이든 접근이 가능하고 변경 사항이 runtime 내에서 영구적으로 반영된다는 점입니다.
그렇기 때문에 객체에 대한 불변, 가변 이야기가 나오는 것이고 프로그래밍 개발 시 함부로 중요한 값을 수정하지 않기 위해 신경 써서 코드를 짜야한다고 말하는 것 아닐까요?
데이터를 다룰 때, 그 데이터가 어떻게 저장되고, 변경되며, 다른 부분과 어떻게 상호작용하는지를 신중하게 고려하는 것이 바로 안전하고 효율적인 코드를 작성하는 핵심이 된다고 생각합니다.
읽어주셔서 감사합니다~~
참고 자료
The Java® Virtual Machine Specification. Java SE 8 Edition
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
Java Virtual Machine Stacks
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.2
Heap
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.3
Youtube
https://www.youtube.com/watch?v=GIsr_r8XztQ&list=PLcXyemr8ZeoRRaTfapB8GMMrLMlCTN4wJ&index=8
'Java' 카테고리의 다른 글
자바의 접근 제어자의 종류와 접근 범위 (3) | 2024.09.23 |
---|---|
자바의 생성자(Constructor)에 대해 알아보기 (4) | 2024.09.23 |