일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- CORS
- 이펙티브자바
- 상속
- 바닐라코딩
- Hibernate Reactive
- 포트폴리오
- 취업회고
- 몰디브
- 클로저
- java
- 소프트웨어장인정신
- SpringSecurity
- jvm
- html
- 2022회고
- til
- http 완벽 가이드
- css
- leetcode
- 신혼여행
- HTTP 완벽가이드
- 부트캠프
- http
- Spring
- 자바
- 주간회고
- 메가테라
- 헤리턴스아라
- JavaScript
- 자바스크립트
- Today
- Total
codingBird
모든 프로그래머가 알아야 하는 JVM - Runtime Data Area 본문
홀 : 그래서 Static멤버는 왜 인스턴스를 생성하지 않아도 사용할 수 있는 거죠?
나 : 클래스가 로딩될 때 method area에 메타데이터가 올라가서...?
홀 : 그럼 그게 어떻게 된다는 거죠?
나 : 모릅니당...
맞다. Method Area에 뭔가가 있다고만 알고 있고 무엇이 들어있는지 실제로 찾아보진 않았다.
그래서 인터넷을 뒤지며 찾았는데도 제대로 된 정보가 나오지 않아 포기를 하려던 도중에 중국어로 작성된 글이 있어 한번 봤는데 꽤나 자세하게 나와있고 작가가 원래 책으로 출판을 하려던 내용을 출판사와 계약이 깨지는 바람에 블로그에 글을 올리게 되었다고 하니 어느 정도 믿을 수 있지 않을까 해서 정리하게 되었다. 참고로 작가는 대만대 컴공과를 졸업하고 퍼블릭 아이비리그인 UIUC에서 석사과정을 마치고 야후, 링크드인을 거쳐 현재 페이스북에 재직 중. 신뢰도 급상승.
다만 중국어를 이해한다고 해도 컴공 쪽 전문용어는 하나도 모르는 상태에서 의역을 하는 것이기 때문에 그저 참고용으로만 읽던지, 뭐 이런 걸 적었어라고 생각한다면 페이지를 닫아도 됩니다 어차피 내가 읽을 용도로 적는 거라.
# 들어가면서
JVM은 메모리를 여러 개의 영억으로 나눴고, 각 영역은 각자 다른 목적을 가지고 생성, 삭제되며 메모리를 공유할 수 있다.
JAVA와 C/C++의 가장 큰 다른 점은 여기서 나오는데 JAVA 프로그래머는 JVM이 메모리를 알아서 관리해주기 때문에 생성된 객체가 삭제되지 않았는지 걱정을 하지 않아도 된다는 점이다. C/C++ 프로그래머는 코드를 작성할 때 메모리를 어떻게 할당하고 관리할 건지 작성을 해야 해서 문제가 생긴다면 debug를 진행해 문제를 찾을 수 있지만 JAVA는 JVM이 메모리를 자동으로 관리해주기 때문에 문제가 발생했을 때 JVM을 이해하고 있지 않다면 debug를 진행할 수 없을 것이다.
# Promgram Counter(PC)
현재 스레드에서 실행 중인 명령어의 주소를 저장.
반복문, 분기문 등 제어 흐름을 하는 코드 때문에 다음 실행해야 하는 명령어의 주소는 바로 이어지는 명령어가 아니라 다른 명령어일 수 있다. 때문에 PC에 저장되어 있는 데이터를 변경함으로 다음에 실행되어야 하는 명령어를 확인한다.
각 CPU나 CPU 코어는 한 번에 하나의 스레드의 명령만 실행할 수 있다. 따라서 스레드는 CPU를 얻기 위해 경쟁하고, CPU는 이 스레드 저 스레드를 번갈아가면서 스레드의 명령어를 실행하게 되는데, 이때 A스레드를 작업하다 다시 B스레드로 돌아가서 이전 작업을 계속 진행하고 싶을 때 명령어를 어디까지 실행했는지 알려주는 것이 PC의 목적이다.
PC는 스레드 별로 가지고 있으며, JAVA메서드를 실행하고 있을 때 PC는 메서드 영역의 바이트코드의 물리적 주소를 가리키고, Native메서드를 실행 중일 땐 null을 가리킨다.
# JVM Stack
스레드 별로 가지고 있으며 생명주기는 스레드와 같다. 스레드에서 메서드를 호출하면 하나의 Stack frame을 생성되고 Stack에 넣어지게 되고 메서드 호출이 종료되면 Stack에서 pop이 된다. LIFO 구조.
A메서드가 호출되면 A메서드의 Stack frame은 Stack에 넣어지게 되고, 만약 A메서드에서 B메서드를 호출했다면 같은 방식으로 B메서드의 Stack frame이 생성되면서 A메서드의 Stack frame위에 올려지게 된다. 그리고 B메서드 호출이 종료되면 B메서드의 Stack frame은 Stack에서 pop 되고 다시 A메서드가 실행되는 방식.
메서드 호출 종료 만이 Stack frame이 pop을 시킬 수 있는 유일한 방법은 아니다, 메서드에서 Error를 던졌을 때, Error에 대한 처리를 하지 않는다면 해당 메서드의 Stack frame은 pop이 된다.
각 스레드 별로 가지게 되는 Stack의 크기는 -Xss에 파라미터를 전달해서 정할 수 있다, 그럼에도 불구하고 메서드가 정해진 크기를 넘어 Stack 메모리를 사용하게 되면 StackOverflowError를 던지지만, 대부분 Stack은 동적으로 크기가 확장될 수 있기 때문에 확장을 하다 메모리에 더 이상 공간이 없다면 OutofMemoryError를 던지게 된다.
# Stack frame
메서드가 호출될 때 메서드에 대한 정보는 Stack frame에 저장되고 해당 스레드 Stack의 최상단에 push 된다.
Stack frame에 저장되는 정보는 아래와 같다.
- Local Variables
- Operand Stack
- Constant pool references
- Return address
# Local Variables
Stack frame에 있는 Local variables는 배열에 담겨서 저장되고 배열의 각 slot은 4 byte다.
해당 배열은 컴파일 타임에 이미 이미 알고 있는 기본 타입(boolean, byte, car, short, int, float, long, double)과 reference타입을 저장한다. long과 double은 두 개의 slot을 차지하는 것을 제외하면 모든 데이터는 1개의 slot에 저장되고, 만약 해당 데이터의 타입이 기본 타입이라면 slot에는 값이 저장되고 참조 타입이라면 slot에는 해당 데이터가 참조하는 객체의 heap 참조 주소가 저장된다.
메서드가 instance method라면 배열의 첫 번째 slot(Index 0)은 해당 instace가 들어가고("this" reference) 그 뒤로 메서드의 패라미터들이 저장된다, 만약 static method라면 첫 번째 slot(Index 0)부터 해당 메서드의 파라미터가 저장됨.
이런 설계는 굉장히 직관적이다, 각 Local variables의 첫 번째 slot에 instance의 참조를 넣음으로써 메서드가 실행될 때 해당 instance의 값을 쉽게 변경할 수 있게 해 준다.
Local variables의 크기는 컴파일 타임에 결정되고, 런타임에서 크기가 변하지 않는다.
# Operand Stack
간단하게 설명하자면 Operand Stack은 레지스터의 Stack이라고 생각하면 되고, 이 레지스터들을 이용해 메서드의 명령어를 계산할 수 있다. 예를 들면 아래와 같다.
위와 같은 코드는 어떻게 작동되는 걸까. 먼저 하나의 레지스터에 1을 저장하고, Local variables 배열에 저장. 그리고 다른 레지스터에 2를 저장하고 Local variables 배열에 저장. 저장이 모두 끝났으면 Local variables 배열에서 두 변수를 읽어 하나의 레지스터에 넣고 연산을 한 다음 다시 Local variables에 넣게 된다.
![]() |
![]() |
* 참고
1. Operand Stack은 스택이기 때문에 iadd 명령은 stack의 최상단 2개를 연산하고 연산 값을 다시 stack에 저장한다.
2. 해당 예시는 마침 Local variables의 가장 앞에 있는 3개의 변수를 사용한 예시이고, 만약 해당 메서드가 instace메서드라면 Local variables의 0번째 index는 intance 자신이 들어있을 것이고 메서드가 input 파라미터를 받는다면 input 파라미터의 순서는 xyz 3개의 Local variables의 순서보다 앞에 있을 것.
# 메서드 동적 링킹
Stack frame은 해당 메서드가 참조하고 있는 Runtime Constant Pool에 대한 참조에 대해 참조를 갖는다.
Constant pool에는 많은 심볼릭 레퍼런스가 있는데 심볼릭 레퍼런스들은 클래스 로딩의 Resoultion 단계에서 직접적인 레퍼런스로 (힙 메모리 영역에 있는 인스턴스 레퍼런스) resoltion 된다. 이것을 Static resolution이라 하고, 몇 심볼릭 레퍼런스는 처음 사용이 될 때 동적으로 링킹이 되는데 이것이 동적 링킹이라고 한다. Stack frame에서 메서드가 동적으로 참조하는 심볼릭 레퍼런스를 참조하고 있어야 JVM이 dynamic resolution을 할 수 있다.
# Return address
해당 메서드 호출이 종료되었을 때 돌아가야 하는 주소를 담고 있다. PC가 해당 주소를 보고 어디로 돌아가야 하는지 알 수 있다.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
12 -> 34 -> 56
stack과 메서드 영역에 있는 클래스 메타데이터의 참조 그리고 heap의 인스턴스 생성을 잘 파악하면서 보자.
Local vairables을 보면 bar foo는 모두 인스턴스 메서드여서 index 0에 this가 들어가 있다.
# Method Area
클래스 로더로 인해 로딩된 클래스들이 저장되는 곳, 모든 스레드가 공유하며 Method area도 GC의 대상이 될 수 있다.
클래스가 로딩되면 클래스 파일은 Heap 영역에 저장되고 해당 클래스에 대한 메타데이터는 Method 영역에 저장되며,
메타데이터는 8가지 종류가 있다.
# Runtime Consatant Pool
Type, Field, Method의 모든 Symbolic refercent를 포함해 모든 상수 정보를 가지고 있으며 배열처럼 index로 접근이 가능하다.
Runtime Constant Pool은 클래스의 constant pool과 비슷하지만 Runtime Constant Pool은 동적 속성을 가지고 있어 컴파일 타임에서 클래스의 Constant Pool을 저장할 수 있을 뿐만 아니라 런타임에서도 동적으로 새로운 Constant Pool을 추가할 수 있다.
# Type informaton
- Type의 전체 이름 - packageName.typeName. 예 : java.lang.Object
- Type의 클래스 / 인터페이스 여부
- 부모 클래스의 전체 이름. Class는 다중 상속이 없기 때문에 Class Type이라면 부모 클래스 하나. Interface라면 모든 부모 인터페이스 이름이 순서대로 저장되어 있다.
- Type의 제어자(modifier) - public, final, abstract
# Field Information
클래스의 모든 필드 (클래스의 변수)는 모두 Method Area에 저장된다.
- 필드 이름
- 필드 타입
- 필드 제어자 - public, private, static, final, transient, vloatile
여기서 잠깐, instance variable의 참조는 stack에 저장되는 건 너무 유명하다. 그러면 static variable의 참조는 어디에 저장되어 있을까? 맞다, static 데이터는 해당 타입의 객체가 공유하는 내용이기 때문에 바로 Method Area에 저장되어 있다.
(여기 홀맨님 질문에 대한 답변이 있다. 바로 static필드의 참조는 Method Area에 있어 해당 타입의 객체가 공유하기 때문에 instance를 생성하지 않아도 참조를 얻을 수 있었던 것, 그래서 바로 사용이 가능한 것)
![]() |
![]() |
# Method Information
물론 타입의 모든 메서드도 저장되어야만 한다.
- 메서드 이름
- 메서드 반환 타입
- 메서드의 모든 파라미터 - Type과 순서 포함
- 메서드 제어자 - public, private, static, final, synchronized, natvie, abstract
- 메서드 구현 부분 - native나 abstract이 아닌 메서드는 메서드의 bytecode를 알고 있어야 PC가 어떤 명령어에서 어떤 명령어를 실행하려면 무엇을 해야 하는지 알 수 있다.
- Stack frame의 Operand stack과 Local variables의 크기
# Class variable -> static 키워드로 선언된 변수
Type의 static변수는 모든 인스턴스에 공유되며 인스턴스가 없어도 직접 접근이 가능하다.
클래스의 변수는 final과 non-final로 나뉘는데 non-final은 클래스 로딩 prepare단계에서 미리 할당한 ( non-final을 위해 메모리 공간을 준비하고 default값을 설정한 곳)에 할당이 된다. final변수는 Runtime Constant Pool에 저장이 되거나 직접 메서드의 bytecode를 복사한다.
# Method table
Type의 모든 instance method는 Method table에 entry를 가지고 있다. 따라서 JVM 어떤 메서드를 실행할 수 있는지 빠르게 알 수 있게 된다. (부모 클래스에서 상속받은 메서드이거나, 자기 자신의 메서드이거나)
# Reference to ClassLoader table
만약 사용자 정의 클래스 로더로 해당 Type을 로드했다면 여기에 해당 클래스 로더 pointer를 넣어야 한다 (아마 참조값).
Dynamic Resolution로 해당 Type의 메서드를 Resolution 해야 된다면 동일한 클래스 로더로만 작업해야 되기 때문에.
# Refernece to Class object
모든 Type은 heap영역의 Class object을 가리키는 포인터가 있어 Class object을 이용해 instance를 생성할 수 있다.
아래 사진은 클래스 로더가 인스턴스 생성을 위해 클래스 로드 여부를 판단하는 과정을 보여주고 있다.
첫 번째 마름모 꼴에서 클래스 로더는 heap영역에 클래스 파일이 있는지 탐색하는데, heap 영역을 직접 탐색하는 것이 아니라 Method Area에 해당 class가 있는지 없는지 찾는다. Method Area에 해당 class가 있다면 heap에는 클래스 파일이 있다는 의미로 바로 클래스를 로드하지 않고 바로 클래스 파일의 참조를 전달할 수 있다. (그리고 프로그램은 클래스 파일로 인스턴스를 생성한다거나 할 수 있다)
# Heap
모든 스레드가 공유하는 Heap은 인스턴스를 저장하고 JVM에서 가장 큰 영역을 차지한다.
JVM이 실행과 동시에 생성되고 Class 파일을 비롯해 모든 인스턴스와 레퍼런스 변수가 저장된다.
그리고 GC가 메모리를 관리하는 곳인데 그 내용은 해당 글의 Garbage Collector부분을 참고하면 된다. -> https://ddurubird.tistory.com/43
# String pool
퀴즈. 위 코드는 몇 개의 객체를 생성했을까?
답은 2개다. new 키워드로 생성된 문자열은 먼저 heap에 생성되고, string pool에 없다면 string pool에도 생성되기 때문.
그럼 이거는?
답은 3개다. 왜 그런지 모르겠다면 해당 글의 문자열 부분을 참고하면 된다. -> https://ddurubird.tistory.com/43
# 스택, 힙, 메서드 영역의 콜라보
힙 영역에는 객체가 할당되고, 객체의 참조 값은 스택에 할당되며, 기타 Class와 연관된 것들은 메서드 영역에 할당된다는 것은 누구나 다 아는 사실이다. 물론 너가 클래스 로더에 대해 공부를 했다면 힙 영역에는 인스턴스뿐만 아니라 클래스 파일 또한 저장되는 것을 알고 있겠지만.
클래스의 모든 메서드의 이름, modifer, 내용이 메서드 영역에 할당되는 것은 매우 직관적이다. 하나의 클래스는 무한히 많은 인스턴스를 가질 수 있는데 힙 영역에 할당되는 인스턴스에 모든 메서드나 필드의 데이터가 들어가는 건 불가능하니 각 인스턴스에 모든 데이터를 저장하는 대신 메서드 영역에 클래스와 연관된 데이터를 할당해서 클래스에 연관된 데이터를 참조하고, 힙에는 인스턴스의 데이터만 할당하는 것이다.
프로그램이 처음 Hello 클래스 파일을 사용한다고 가정해보자.
이때 클래스 로더는 먼저 힙 영역에 Hello 클래스 파일이 있는지 확인하고 만약 없다면 클래스 파일 로딩을 시작하고, 로딩이 끝나면 클래스 파일을 힙 영역에 두고 메타데이터는 메서드 영역에 저장한다.
메서드영역에 있는 Hello의 클래스 데이터와 힙 영역에 있는 클래스파일은 서로 참조를 하고 있다.
그리고 heap영역에 인스턴스를 생성하면 아래와 같은 그림이 된다.
그리고 프로그램이 다시 Hello 클래스가 필요하다면
클래스 로더는 Hello 클래스 파일이 힙 영역에 있는 것을 확인하고 클래스 파일을 다시 로드할 필요가 없다고 판단한다.
따라서 로드를 하지 않고 바로 인스턴스를 생성할 수 있다.
이 상황에서 프로그램이 처음으로 Word클래스를 호출하게 된다면. 아래 그림과 같은 상태가 된다.
*world의 클래스 파일과 인스턴스가 힙 영역에 할당되고 스택에는 world 인스턴스의 참조가, 메소드 영역에는 world의 데이터가 생성되었다.
# Native Method Stack
Java로 작성된 프로그램을 실행하면서, 순수하게 Java로 구성된 코드만으로는 사용할 수 없는 시스템의 자원이나 API가 존재한다. 다른 프로그래밍 언어로 작성된 메서드들을 Native Method라고 부르는데, Native Method Stack은 JVM stack과 같이 Native Method가 실행되면 stack이 Native Method Stack에 쌓이게 된다.
* Java와 C언어는 중간에 있는 JNI(Java native method interface)를 통해 서로 호출이 가능하며, Native Method 들은 JML (Java native method libraries)에 저장되어 있다.
후기 :
원래는 메서드 영역만 정리하려고 했는데 다른 영역도 너무 정리가 잘 되어 있어서 나중에 참고하면 좋을 듯해서 전부 번역을 했다.
이 또한 추석이기에 시간이 있어 가능했던 것. 추석 만세!
본문 :
每個程序員都該瞭解的JVM - 運行時數據區 (모든 프로그래머가 알아야 하는 JVM - Runtime Data Area) https://www.jyt0532.com/2020/03/11/runtime-data-area/
'JAVA' 카테고리의 다른 글
Spring Application - CORS 설정 - 2 (0) | 2023.07.11 |
---|---|
Spring Application - CORS 설정 - 1 (0) | 2023.07.11 |
[이펙티브 자바] - Item 2 생성자에 매개변수가 많다면 빌더를 고려하라. (0) | 2023.04.15 |
[이펙티브 자바] - Item 1 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2023.04.14 |
Favor composition over inheritance. (0) | 2022.09.06 |