[Java] JVM 메모리구조 암기 말고 이해하기(중요한 것은 꺾이지 않는 마음)

토끼는 개발개발·2024년 10월 17일
0

Java

목록 보기
2/33
post-thumbnail

자료출처: https://blog.naver.com/chlwlstjd10/222239136195

우리가 알아 볼 JVM의 메모리 영역은 런타임 데이터 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.

💡 먼저! 단순히 CS 지식 혹은 면접을 위해 JVM 메모리 구조를 달달 외우러 들어오신 분이 계시다면 암기는 아무 의미가 없다고 말씀드리고 싶다.

우리가 JVM메모리 구조를 알아야 하는 이유 또 이것을 굳이 면접에서 물어보는 이유는 JVM 메모리 구조를 알아야 Java 코딩의 원리가 이해되기 때문이다. 단순히 'static을 붙이면 객체 생성없이 쓸 수 있다.'로 방법만 아는 것이 아니라, '왜 static을 붙이면 객체 생성없이 쓸 수 있는가?'라는 질문에 답 할 수 있게 되는 것이다.

하단의 메모리 구조 부분만 외우는 것이 아니라, 포스팅을 끝까지 쭉 따라가며 이해하면 자연스럽게 메모리 구조를 설명할 수 있게 될 것이다.



📌 JVM 메모리 구조

런타임 데이터 영역은 위 그림과 같이 크게 Method 영역, Heap 영역, Stack 영역, PC Register, Native Method Stacks로 나눌 수 있다.

  • Method영역 : JVM에서 읽어들인 클래스와 인터페이스에 대한 런타임 상수 풀, 메서드와 필드, Static 변수, 메서드 바이트 코드 등을 보관.
  • Heap 영역 : 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 메모리 영역 New 연산자를 통해 생성한 객체 또는 인스턴스와 배열을 저장.
  • Stack 영역 : 선입후출(FILO) 구조, 메서드 호출 시 생성되는 스레드 수행정보를 기록하는 Frame 저장, 메서드 정보, 지역변수, 매개변수, 연산 중 발생하는 임시 데이터 저장.
  • PC 레지스터 : 현재 실행중인 JVM 주소를 가지고 있다. CPU 명령이 즉 instruction을 수행한다. CPU instruction을 수행하는 동안 필요한 정보를 CPU내 기억장치인 레지스터에 저장, 연산 및 결과값을 메모리에 전달하기 전 CPU 내 기억장치.
  • Native Method Stack Area : java 외 언어로 작성된 네이티브 코드를 위한 메모리. C/C++ 등의 코드를 수행하기 위한 스택. native 메서드의 매개변수, 지역변수 등을 바이트 코드로 저장.

메모리 구조에 대한 설명은 위와 같다.

무슨 말인지 도통 다가오질 않는다면 포스팅을 쭉 읽고 다시 위로 올라와서 읽어보면 좀 더 이해가 될 것이다.

중요한건 꺾이지 않는 마음^^

이제 영역별로 하나씩 파헤쳐보자.



1. Method 영역

메서드 영역(Method Area)은 JVM의 런타임 데이터 영역의 일부로, 모든 스레드가 공유하는 메모리 공간이다. 이 영역은 JVM이 클래스를 로드할 때 메모리 공간을 할당받으며, 클래스의 구조 정보, 필드, 메서드, 메서드 코드, 정적 변수(static), 런타임 상수 풀 등이 저장된다.

1) 클래스 로딩과 함께 초기화
메서드 영역은 JVM이 클래스를 처음 로드할 때 한 번만 해당 클래스의 정보를 메모리에 올리고, 이 정보는 프로그램이 종료될 때까지 메모리에 남는다. 여기서 저장되는 정보에는 클래스 이름, 부모 클래스, 메서드 정보, 필드 정보, 인터페이스 정보 등이 포함된다.


2) 정적 변수(static variables)의 저장
클래스에 선언된 정적 변수(static)는 메서드 영역에 저장되며, 이 변수는 해당 클래스의 모든 인스턴스에서 공유된다. static 변수는 한 번 할당되면 클래스 로딩 시 메모리에 올라가고, 프로그램이 끝날 때까지 유지된다. 정적 변수는 객체 생성과 관계없이 클래스 레벨에서 관리되며, 객체 없이도 클래스명.staticVariable 형태로 접근할 수 있다.


3) 공유되는 데이터
메서드 영역의 데이터는 모든 스레드에서 공유된다. 스레드가 메서드를 호출하거나 클래스 정보를 참조할 때, 메서드 영역에 있는 데이터를 함께 사용한다. 이 정적 특성 덕분에 모든 스레드는 클래스의 정적 메서드와 정적 변수에 동일한 방식으로 접근할 수 있다.


4) 메모리 할당 및 해제
메서드 영역은 동적 메모리 할당이 아니라, 클래스 로딩 시 필요한 만큼만 할당되고 프로그램이 끝날 때까지 변하지 않는다. 힙 영역과 달리 가비지 컬렉션의 대상이 아니며, 클래스가 언로드될 때만 메모리가 해제된다. 잘못된 클래스 로딩이나 언로드가 없으면, 프로그램이 종료될 때까지 유지된다.


5) 변경되지 않는 특성
한 번 메서드 영역에 로드된 클래스 정보나 정적 변수는 프로그램 실행 중 변경되지 않는다. 클래스가 메모리에 로드된 후에는 동일한 데이터를 참조하며, 여러 스레드가 동시에 접근해도 동일한 데이터를 참조하므로, 메모리 사용이 효율적이고 일관성이 유지된다.


💡 쉽게 말하면, 모두가 공유하고 한 번 올라오면 종료될때까지 남아있는 영역이다.

이제 위에서 던진 '왜 static을 붙이면 객체 생성없이 쓸 수 있는가?'에 답할 수 있을 것이다.
정적변수는 Method영역, 지역변수는 Stack영역, 인스턴수 변수는 Heap영역에 저장된다.
정적변수(static)가 저장되는 Method 영역은 모두가 공유하고, 한 번 올라오면 종료될때까지 변하지 않고 남아있으므로 객체 생성없이 쓸 수 있는 것이다.




2. Heap 영역

Heap영역은 자바 가상 머신(JVM)에서 동적으로 할당된 객체와 배열을 저장하는 메모리 영역이다. 모든 클래스 인스턴스와 배열은 힙에 저장되며, Garbage Collector가 힙을 관리하여 사용하지 않는 객체를 자동으로 제거한다. 힙 메모리는 JVM에서 중요한 메모리 풀 중 하나로, 동적으로 크기가 조정된다.

1) 동적 객체 할당
객체는 힙 영역에 할당된다. 예를 들어, new 키워드를 사용해 객체를 생성할 때 그 메모리는 힙 영역에 할당된다.

Person p = new Person("토끼는개발개발", "99");
//     ↓         ↓
// Stack Area   Heap Area

유의할점은 힙 영역에 생성된 객체와 배열은 Reference Type으로서, JVM 스택 영역의 변수나 다른 객체의 필드에서 참조된다는 점이다. 즉, 힙의 참조 주소는 "스택"이 갖고 있고 해당 객체를 통해서만 힙 영역에 있는 인스턴스를 핸들링할 수 있는 것이다.


2) 가비지 컬렉션(Garbage Collection)
힙 메모리는 가비지 컬렉터에 의해 관리되며, 사용되지 않는 객체가 메모리에서 자동으로 해제된다. 이는 메모리 누수를 방지하고 효율적으로 메모리를 관리하는 데 중요한 역할을 한다.
가비지 컬렉터가 어떻게 작동하는지는 추후에 자세히 포스팅하도록 하겠다.(포스팅 후 링크 예정)

3) 가변적 크기
힙의 크기는 JVM이 시작될 때 설정한 값에 따라 달라지며, 필요에 따라 자동으로 조정된다.
즉, 힙 영역의 크기는 애플리케이션의 실행 도중에 동적으로 늘어나거나 줄어든다.


4) 메모리 풀 관리
JVM은 힙을 여러 개의 메모리 풀로 나누어 관리할 수 있다.
각각의 풀은 객체 할당과 가비지 컬렉션을 수행하는 역할을 한다.


💡 힙은 동적 영역(크기가 가변)으로, 객체를 저장하는 공간이다.
Static 변수와 인스턴스 변수의 차이를 메모리 구조 중점으로 생각해보자.
인스턴스 변수는 Heap영역에 저장된다. 각 객체마다 따로 메모리를 할당받고, 객체가 살아있는 동안만 메모리에 존재한다. 객체가 삭제되면 메모리에서 해제된다.



3. Stack 영역

Stack 영역은 메서드 호출 시마다 생성되는 스택 프레임을 저장하는 공간이다. 각 스레드는 자신의 스택을 가지며, 메서드 호출에 따라 스택 프레임이 쌓이고, 메서드가 종료되면 해당 프레임이 제거된다. 메소드 내에서 정의하는 기본 자료형(int, boolean 등)에 해당되는 지역변수, 매개변수의 데이터 값이 저장된다.

1) 스택 메모리의 역할
스택 메모리는 각 스레드에 독립적으로 할당된다. 메서드 호출 시 메서드 스택 프레임이 쌓이며, 이 프레임은 지역 변수와 호출 정보를 저장한다. 메서드가 종료되면 해당 스택 프레임은 메모리에서 자동으로 제거된다.

하나의 메서드당 하나의 스택 프레임이 필요하며, 메서드를 호출하기 직전 스택 프레임을 스택 영역에 생성한 후 메서드를 호출하게 된다. 스택 프레임에 쌓이는 데이터는 메서드의 매개변수, 지역변수, 리턴값 등이 있다. 메서드가 종료되면 제거된다.


2) 메서드 호출 시 처리(FILO 선입후출)
메서드가 호출될 때마다 스택 프레임이 푸시(push)되어 메서드가 실행된다. 메서드가 완료되면 그 프레임은 팝(pop) 되어 메모리에서 제거된다. 이는 메서드 실행에 필요한 로컬 변수와 매개변수, 반환 주소 등을 저장하는 데 사용된다.


3) 메모리 크기 제한
스택 메모리는 각 스레드별로 고정된 크기를 가지며, 과도한 메서드 호출이나 재귀 호출이 발생하면 스택 오버플로우(Overflow)가 발생할 수 있다. 이 제한 때문에 스택은 힙 메모리에 비해 상대적으로 적은 용량을 사용한다.


간단하게, 메서드 수행시 임시적으로 사용되는 변수나 정보들이 저장되는 영역이다.
지역변수가 저장되는 곳이 바로 이 스택영역이다.
위에서 static변수는 메서드 영역, 인스턴스 변수는 힙 영역에 저장된다고 했다.
스택 메모리는 메서드 호출 시에 스택 프레임을 할당하고, 메서드 호출이 끝나면 해당 스택 프레임은 제거되므로, 지역 변수의 수명도 메서드 호출 동안만 지속된다.

지역변수와 인스턴스 변수의 차이를 메모리 구조를 중점으로 이해해보자.

먼저, 인스턴스 변수이다. Heap영역에 존재한다.
Car 객체 내부에 정의된 move() 메서드를 호출하면서 메서드가 Stack 영역에 새로운 스택 프레임으로 추가된다. 이 때 this 라는 암묵적인 변수가 자동 생성되게 되는데, 이 this 변수는 자동으로 힙 영역에 있는 Car 객체를 가리키게 된다. 따라서, move() 메소드 안의 코드 position++이 동작하면서 힙 영역에 있는 인스턴스 변수 position 값이 변하게 된다.

이에 반해 지역변수는 Stack영역의 스택프레임 안에 존재한다.
메서드 호출이 끝나면 스택프레임은 제거됨에 따라 함께 제거된다.


4. PC 레지스터 (PC Register)

PC(Program Counter) 레지스터는 각 스레드가 실행할 바이트코드 명령의 주소를 저장하는 작은 메모리 공간이다. JVM에서 각 스레드는 자신만의 PC 레지스터를 가지며, 이 레지스터는 현재 실행 중인 메서드에서 다음에 실행될 명령어의 주소를 가리킵니다.

1) 쓰레드별로 독립적
JVM에서는 여러 쓰레드가 동시에 실행되는데, 각 쓰레드는 자신만의 PC 레지스터를 가진다. 이는 쓰레드가 현재 실행 중인 명령어 주소를 저장하기 때문이다.

2) 현재 실행 중인 명령어 주소를 저장 PC 레지스터는 현재 쓰레드가 실행 중인 JVM 명령어의 주소를 저장한다. 각 쓰레드가 어떤 명령어를 실행하고 있는지 추적한다.

3) 바이트코드 실행을 제어
JVM은 바이트코드를 해석하고 실행하는데, PC 레지스터는 각 쓰레드가 어떤 바이트코드를 실행할지 결정한다. 명령어 실행 후 다음에 실행할 명령어의 주소를 기록하거나, 점프 명령에 의해 주소가 변경된다.

4) 네이티브 메소드 실행 시에는 사용되지 않음
만약 JVM이 네이티브 메소드를 실행 중이라면, 해당 쓰레드는 PC 레지스터를 사용하지 않는다. 네이티브 메소드는 JVM 외부에서 실행되기 때문에 PC 레지스터의 역할이 필요하지 않다.



5. Native Method Stacks (네이티브 메서드 스택)

네이티브 메서드 스택은 자바가 아닌 네이티브 언어(C, C++ 등)로 작성된 메서드가 실행될 때 사용하는 메모리 공간이다. 네이티브 메서드는 JVM 외부에서 실행되므로, JVM 스택과는 별도의 네이티브 메서드 스택을 사용한다. 각 스레드는 자신의 네이티브 메서드 스택을 가지고 있다.

1) 네이티브 코드 실행
네이티브 메서드 스택은 자바가 아닌 플랫폼 종속적인 네이티브 코드를 실행할 때 사용된다. JVM이 자바 바이트코드를 실행할 때는 자바 스택을 사용하지만, 네이티브 코드를 호출할 때는 이 스택이 필요하다.

2) JNI(Java Native Interface)
JVM은 주로 JNI를 통해 네이티브 메서드를 호출한다. JNI는 자바 코드가 네이티브 라이브러리와 상호작용할 수 있도록 해주는 인터페이스다. 네이티브 메서드가 호출되면 해당 메서드의 실행을 위해 네이티브 메서드 스택이 사용된다.

3) 스택 프레임 관리
자바 메서드 호출 시 JVM이 자바 스택에서 스택 프레임을 관리하는 것처럼, 네이티브 메서드 호출 시에도 네이티브 메서드 스택에서 스택 프레임이 관리된다. 이 스택 프레임에는 네이티브 메서드의 지역 변수와 중간 연산 결과 등이 저장된다.

4) 플랫폼 의존적
네이티브 메서드 스택은 자바 스택과 달리, JVM 구현에 따라 크기나 동작 방식이 달라질 수 있다. 또한, 네이티브 메서드는 운영 체제나 하드웨어 아키텍처에 따라 다르게 구현되므로, 네이티브 메서드 스택도 플랫폼에 종속적이다.



이렇게 JVM의 메모리 구조에 대해 알아보았다.
여기서 우리가 중점적으로 봐야하는 것은 '정적'과 '동적'에 대한 차이이다.

  • '정적 메모리' - Method Area
    컴파일 시점에 할당되는 메모리.
    클래스 로딩 시 고정된 메모리를 차지하고 프로그램이 종료될 때까지 유지되며, static 변수나 메소드와 같이 고정적인 데이터를 저장하는 것을 말한다.
  • '동적 메모리' - Stack Area, Heap Area
    런타임 시점에 할당되는 메모리.
    런타임 시에 객체 생성, 메소드 호출 등으로 필요할 때마다 할당 및 해제가 이루어지며, 힙과 스택을 통해 관리되는 것을 말한다.

이렇게 정적 메모리와 동적 메모리는 JVM에서 각각의 용도에 맞게 나뉘어 관리되면서 프로그램의 안정적인 실행을 지원한다.




오늘의 한 줄

벼르고 벼르던 JVM 메모리 구조 포스팅을 드디어 했다.
사실 이렇게 정리해도 '그래서 이걸 왜 알아야 하는데? 어디에 도움이 되는건데?'라고 생각 할 수 있다.

이번 포스팅에서는 쉽게 정적 변수, 인스턴스 변수, 지역 변수로 설명했지만 이후 상속과 다형성을 이해할 때 이 메모리 구조는 아주!아주! 큰 도움이 된다.

당장 상속에서 정적메서드를 오버라이딩 할때 왜 '메서드 하이딩'을 수행하는지,
메서드 호출시 정적, 동적 디스패치는 왜 그리고 어떻게 수행되는지 등등 메모리 구조와 특징, 메커니즘을 이해해야 이후 갈 길이 좀 더 매끄러워진다.

공부해가는 입장으로 말해보자면, 메모리 구조는 아는만큼 보이고 재밌다.
java에 대해 공부하다보면 깊게 들어갈수록 메모리 구조는 떼놓을 수 없고, '아!' 하는 순간들이 찾아 올 것이다. 그리고 계속 메모리 구조로 돌아와서 보고 또 볼 것이다.
내가 포스팅을 작성한 이유도 저장해놓고 두고두고 보기위해서이다..


**추가적인 내용들(상속과 메모리 구조 등)도 차후에 포스팅해서 이 포스팅 하단에 링크를 걸어 둘 예정입니다!


출처

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html
https://docs.oracle.com/en/database/oracle/oracle-database/18/jjdev/about-Java-memory-usage.html
https://inpa.tistory.com

profile
하이 이것은 나의 깨지고 부서지는 기록들입니다

0개의 댓글