자바 메모리 구조와 static

런타임 데이터 영역은 JVM이라는 프로그램이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다. 런타임 데이터 영역은 6개의 영역으로 나눌 수 있다.
PC 레지스터(PC Register), JVM 스택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)은 스레드마다 하나씩 생성된다.
힙(Heap), 메서드 영역(Method Area), 런타임 상수 풀(Runtime Constant Pool)은 모든 스레드가 공유해서 사용한다.
PC 레지스터 : PC(Program Counter) 레지스터는 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. PC 레지스터는 현재 수행중인 JVM 명령의 주소를 갖는다. 현재 실행 중인 스레드의 '바이트코드 줄 번호 표시기' , 스레드 전환 후 이전에 실행하다 멈춘 지점을 정확하게 복원하려면 스레드 각각에는 고유한 프로그램 카운터가 필요하다. 각 스레드의 카운터는 서로 영향을 주지 않 독립된 영역에 저장됨.
JVM 스택 : JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은 오직 JVM 스택에 스택 프레임을 추가하고 (push) 제거하는 (pop) 동작만 수행한다. 예외 발생 시 printStackTrace() 등의 메서드로 보여주는 StackTrace의 각 라인은 하나의 스택 프레임을 표현한다.
스택 프레임 : JVM 내에서 메서드가 수행될 때마다 하나의 스택 프레임이 생성되어 해당 스레드의 JVM 스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다. 각 스택 프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다. 지역 변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다.
지역 변수 배열: 0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고,1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다. (메서드의 지역 변수, 매개변수가 스택 영역에서 관리됨, 스택 프레임이 종료되면 지역변수도 함께 제거된다)
네이티브 메서드 스택: 자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉 JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다.
메서드 영역 : 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 실행코드(바이트코드) 등을 보관한다. 메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen)이라고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택사항이다.
런타임 상수 풀 : 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. 메서드 영역에 포함되는 영역이긴 하지만, JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따로 중요하게 기술한다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.
String Constant Pool과 Constant Pool, Runtime Constant Pool 구분
Constant Pool
.class 파일 내부에 존재
모든 종류의 상수 값 : 숫자 리터럴, 문자 리터럴, 문자열 리터럴
모든 종류의 '심볼릭 레퍼런스' : 다른 클래스, 필드, 메서드를 가리키는 이름 정보와 타입 정보들(아직 실제 메모리 주소로 연결된 것은 아님)
Runtime Constant Pool (JVM 메모리 - 메서드 영역/Metaspace)
JVM이 .class 파일을 메모리에 로딩하는 과정에서 .class 파일 안에 있던 'Constant Pool'의 내용을 그대로 JVM의 '메서드 영역(Method Area)' 또는 Metaspace(Java 8 이후)라는 메모리 공간에 옮겨 와서 '실행 가능한 형태'로 가공해둔 것이다.
.class 파일의 Constant Pool에 있던 정보들을 그대로 가져온다.
여기서 심볼릭 레퍼런스들은 '상수 풀 해결(Constant Pool Resolution)이라는 과정을 거치면서 실제 메모리 주소(참조)나 정확한 정보로 연결(바인딩) 될 수 있다.
String Constant Pool (JVM 메모리 -힙 영역)
자바에서 코드에 직접 쓰여진 '문자열 리터럴'이나 String.intern() 메서드를 호출하여 만들어진 '동일한 문자열 값을 가진 String 객체들을 중복 없이 모아두는 특별한 공간'이다.
위치 : Java 6 이하 : 메서드 영역 안에 Runtime Constant Pool의 일부로 존재했다. Java 7 이상 : 힙(Heap) 영역으로 이동했다.
실제
java.lang.String
클래스의 '인스턴스(객체)'들을 담고 있다. 그것도 고유한 문자열 값을 가진 객체들 (예: "hello"라는 리터럴이 코드에 여러 번 나와도, 문자열 풀에는 "hello" 객체가 하나만 만들어져서 재사용된다)
자바에서 특정 클래스로 100개의 인스턴스를 생성하면, 힙 메모리에 100개의 인스턴스가 생긴다. 각각의 인스턴스는 내부에 변수와 메서드를 가진다. 같은 클래스로 부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만, 메서드는 공통된 코드를 공유한다. 따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없다. 메서드는 메서드 영역에서 공통으로 관리되고 실행된다. 정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행한다.
힙 영역
객체(인스턴스)가 생성되는 영역, new 명령어를 사용하면 이 영역을 사용한다.
public class Data {
public String name;
public static int count; // static
}
멤버 변수(필드)의 종류
인스턴스 변수 : static이 붙지 않은 멤버 변수
static이 붙지 않는 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다.
인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어진다.
클래스 변수(정적 변수, static 변수)
static이 붙은 멤버 변수
클래스 자체에 소속되어 있음
클래스 변수는 자바 프로그램을 시작할 때 1개만 만들어짐.
메서드 영역에서 관리된다.
변수와 생명주기
지역 변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거된다.
인스턴스 변수: 인스턴스에 있는 멤버 변수를 인스턴스 변수라 한다. 인스턴스 변수는 힙 영역을 사용한다. 힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지는 생존
GC 발생은 더이상 힙 영역에 있는 인스턴스가 참조 되지 않을 때 발생한다. 이때 힙 영역 외부가 아닌 힙 영역 안에서 인스턴스끼리 서로 참조되는 경우에는 GC의 대상이다.
클래스 변수: 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성된다. 그리고 JVM이 종료될 때 까지 생명주기가 이어진다.
클래스 메서드
메서드 앞에도 static 을 붙일 수 있다. 이것을 정적 메서드 또는 클래스 메서드라 한다. 정적 메서드라는 용어는 static 이 정적이라는 뜻이기 때문이고, 클래스 메서드라는 용어는 인스턴스 생성 없이 마치 클래스에 있는 메서드를 바로 호출하는 것 처럼 느껴지기 때문이다.
main() 메서드는 정적메서드이다.
정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있다. 따라서 정적 메서드인 main() 메서드가 같은 클래스에서 호출하는 메서드도 정적 메서드로 선언해서 사용했다. main() 메서드와 static 메서드 호출
public class ValueDataMain { public static void main(String[] args) { ValueData valueData = new ValueData(); add(valueData); } static void add(ValueData valueData) { valueData.value++; System.out.println("숫자 증가 value=" + valueData.value); } }
인스턴스 메서드
static 이 붙지 않은 메서드는 인스턴스를 생성해야 호출할 수 있다. 이것을 인스턴스 메서드라 한다.
정적 메서드가 인스턴스 기능을 사용할 수 없는 이유
정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스처럼 참조값의 개념이 없다. 특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출된다. 따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다. 객체의 참조값을 직접 매개변수로 전달하면 정적 메소드도 인스턴스의 변수나 메서드를 호출할 수 있다.
Last updated