on
JVM메모리 구조
JVM 메모리 구조
JVM 메모리 구조에는 대표적으로 힙 영역과 메소드 영역으로 유명하지만, 실제로 우리 자바 응용 프로그래머들이 알아야할 것들은 생각보다 많다. 전체적으로는 아래와 같다.
-
heap area
-
static area(method area)
-
pc registers
-
native stack area
-
stack area
heap area
일반적으로 런타임 시에 생성되는 데이터들이 담기는 곳이다.new 생성자 호출을 통해 생성되는 객체들도 여기에 담긴다.(객체도 결국 데이터이고, 객체를 선언한다는 것은 데이터를 담는 자료 구조를 선언한다는 것이다.) heap area 는 지속적으로 GC(가비지 콜렉터)에 의해 해당 데이터가 참조되지 않는 지를 체크해서 할당 해제(청소)를 한다.
static area
구글링 하면 일반적으로 메소드 아레아 라고 많이 얘기하지만, static area 가 정확하다. 이유는 이 녀석이 하는 역활 때문인데, static area 를 보는 관점에 따라서 method area, class area, code area, constant pool 라고 한다. 느낌이 오질 않는다? 소스 상의 변하지 않는 정적인 데이터들이 여기에 담긴다. 런타임 시에 해당 객체의 타입(형이라고도 한다. 즉, class 를 말하는 것이다.)에 대한 정보도 여기서 얻어온다. constant pool 은 대단한 게 아니다. javascript es6 의 const 문법 그것을 의미하는 것과 같다. 즉 상수를 의미하는 데, 대표적인 예로 staing 에 대한 데이터가 여기에 존재한다. 이는 아래에서 자세히 설명한다. 그래서 static(정적) area 라고 하는 것이 옳은 것이다.
-
static 키워드는 어디에 담기는가?
java 8 이전과 이후에 따라 담기는 곳이 달라진다. java 7 이하 버전에서는 static 키워드로 선언 된 변수는 static area 에 담기게 되었다. 그리고 java 8 이후에는 heap area 에 담기게 되었다. 왜 그럴까?
이유는 static area 가 실제로 저장되는 메모리 저장소가 Permanent Generation 이라는 곳에서 관리가 된다. Permanent 란 말을 보면 알겠지만 변하지 않는이란 의미로 정적이라는 것과 같은 의미인데, Permanent 설정은 JVM 이 실행될 떄의 옵션 permSize 에서만 설정되고 이후로 바꿀 수가 없다. 그렇다면 얘기를 바꿔보자. static 키워드로 생성 된 변수가 변하지 않는 Permanent Generation 저장소에 담긴다고 했는 데, 만약 이 static 변수의 용량이 커진다면? 어플리케이션은 죽고 말 것이다.
이 문제는 대부분 자바 프로그래머의 실수로 생겨난 문제가 너무 많았고, Oracle 에서는 극단의 조취로 ‘static 변수를 heap area 에 할당해버리자’ 로 결정을 내리게 되어서 java 8 부터 heap area 에 쌓이게 하도록 구조를 변경했다. 그래서 바뀐 부분은 Permanent Generation 이라는 메모리 저장소를 삭제하고 Native(Metaspace) 라는 메모리 저장소를 새로 구현하였다. Metaspace 영역은 static 변수 외에 기존 static area 의 모든 것이 담기고, 기존 Permanent 에 비해서 동적으로 메모리 할당이 유기적으로 될 수 있도록 바뀌었다. java8 부터는 실제로 perSize 와 같은 옵션이 무시(삭제)되고 XX:MetaspaceSize 나 XX:MaxMetaspaceSize 옵션이라는 것이 생겨난 것을 볼 수 있다.
자세한 것은 DZone 블로그 를 참고하자.
-
constant pool
static area 에서 가장 중요한 개념이다. 우리가 흔히 말하는 객체 타입, 즉 클래스 파일에 작성된 객체의 원형(레퍼런스)에 대한 정보가 담기거나(이 정보는 자바 리플렉션 API 사용시에 처리 된다.), 상수 키워드로 선언 된 데이터가 이곳에 담긴다.
-
jvm 옵션 중에 perm 에 대한 이야기
대부분 maxHeapSize 라는 옵션에 대해서는 많이 들어보았을 것이다. 이는 heapSize 에 대한 설정으로 heap memory 를 얼마나 할당할지에 대한 설정이다. 간혹 maxHeapSize 외에도 permSize 라는 것도 발견할 수 있는 데, 이는
-
new String 의 차이
우리가 문자를 자바에서 변수에 할당할 때, 사용하는 키워드는 대표적으로 아래와 같다.
@Test public void isSame() { String a = "hello"; String b = new String("hello"); boolean isSame = a == b; boolean isEqual = a.equals(b); Assert.assertTrue(isSame); Assert.assertTrue(isEqual); }
a는 일반적으로 사용하는 리터럴 이고, b 는 객체를 생성했다. 그리고 isSame 메소드는 단순히 a 와 b 를 비교하는 메소드이다. 이 테스트의 결과는 어떠할까?
isSame 의 false 이고, isEqaul 은 true 이다. 왜 그럴까?
String 자료형은 특별한 참조형이라고 불린다. 이유는 String 이라는 자료형은 객체이지만, 실제로는 constant pool 에 이미 등록된 글자(char)들의 레퍼런스 주소를 가르키는 것이기 떄문이다. 우리가 String a 를 선언한 hello 라는 문자는 결국 constant pool 에 이미 등록되어 있다. 즉, String a 의 constant pool 에 a~z 까지 등록된 글자들의 참조주소들을 담은 것에 불과한 것이다. 반면 String b 의 경우 constant pool 에 이미 등록된 글자를 사용하지 않고, 새로운 메모리 주소에 h.e.l.l.o 라는 글자들을 등록한 것이기 때문에 레퍼런스를 체크하는 isSame 의 값이 false 가 된 것이다.
-
stack area(thread area, method call area)
일반적으로 메소드 호출 스택이 들어가는 곳으로 알지만, 사실은 thread 관리를 위한 stack 이다. 자바 어플리케이션은 멀티 스레드 프로그래밍을 하지 않는다면 단일 스레드를 생성해서 프로그램이 돌아가게 된다. 생성 된 스레드는 고유의 관리 메모리 주소를 가지게 되는 데, 이가 stack area에 저장된다. 그리고 각 주소에는 메소드 호출 프레임이라는 스택이 생성된다. 즉 main 메소드를 위한 최초의 스레드의 관리를 위한 메모리가 stack area에 생기게 되고, 해당 스레드 내부의 메소드 콜 스택은 관리 된다. 참고로 이 stack area 의 스레드는 스레드 life cycle 관리를 위한 개별 관리일 뿐이고, static area 나 heap area 에는 어떠한 스레드도 다 접근이 가능하기 때문에 멀티 스레드 환경에서의 코딩기법을 주의해라는 것이다.
실제로 개별 스레드 관리 메모리 내부에 생성되는 메소드 프레임에는 각 호출 메소드 별 스택에 독립적인 변수 관리 메모리가 생성 되는 데, 이곳에서 메소드 내부의 지역 변수가 관리된다. 즉, 멀티스레딩 코딩을 위한 여러 조언들 중에 지역 변수를 사용하라는 의미는 stack area의 개별 스레드 내부의 메소드 프레임 영역에서 변수를 생성해라는 의미와 같다.
-
자바 어플리케이션이 실행되는 과정
자바 어플리케이션이 실행되는 과정에 대해 알아보자. 자바는 실행 커맨드로 실행 할 메인클래스를 인자로 넘겨 받는다. 실행 커맨드에는 다양한 커맨드가 있지만 대표적으로는 아래의 커맨드를 주로 사용한다. 다양한 방법에 대해서는 여기를 참조하거나 직접 알아보라.
> java MyApp.class > java MyApp.java
-
JVM 은 MyApp.class 의 소스파일을 읽어들인다.
-
MyApp.class 파일을 ROOT 로 두고, 클래스 로더에 의해 클래스 내부에서 참조(import)하는 모든 클래스들을 읽어들인다.
-
static 키워드와 class 정보(해당 클래스의 필드(멤버변수), 메소드 정보 등)들을 JVM의 static area 에 올리게 된다.
-
MyApp.class 에서 psvm(public static void main(Object args..){}) 이라고 하는 main 메소드를 찾는다.
-
최초의 스레드를 생성하고, stack area 에 올린다. main 메소드를 해당 스레드의 프레임 스택에 추가한다. 이후 main 메소드 안의 위에서 아래로 순차적으로 main 메소드 내부의 호출되는 메소드들을 프레임 스택에 쌓는다.
-
JVM 의 execution engine 은 이 stack area에 있는 스레드와 스레드의 프레임들을 하나씩 꺼내어서 실행하게 된다.
이것이 우리가 아는 자바 어플리케이션이 실행되는 과정이다.
인텔리 J 디버그창에서는 좌측 Frames 에 호출 스택의 프레임이 나타난다. 그리고 호출 스택을 클릭시에 우측에는 프레임 안의 변수들의 정보를 볼 수 있다.
위 캡처에서 호출스택을 보면 JUnitStater.class 에서 main 메소드가 존재함을 알 수가 있다. 그리고 최종적으로 JVM에서 가장 처음에 실행되는 (마지막에 스택에 들어간 것은) FamilyServiceTest 클래스의 multiplyRate() 메소드이다.
multiplyRate:27, FamilyServiceTest (com.fourones.cms.mpoker.application.gameConfig.clubFamily.service) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) runReflectiveCall:50, FrameworkMethod$1 (org.junit.runners.model) run:12, ReflectiveCallable (org.junit.internal.runners.model) invokeExplosively:47, FrameworkMethod (org.junit.runners.model) evaluate:17, InvokeMethod (org.junit.internal.runners.statements) runLeaf:325, ParentRunner (org.junit.runners) runChild:78, BlockJUnit4ClassRunner (org.junit.runners) runChild:57, BlockJUnit4ClassRunner (org.junit.runners) run:290, ParentRunner$3 (org.junit.runners) schedule:71, ParentRunner$1 (org.junit.runners) runChildren:288, ParentRunner (org.junit.runners) access$000:58, ParentRunner (org.junit.runners) evaluate:268, ParentRunner$2 (org.junit.runners) run:363, ParentRunner (org.junit.runners) run:137, JUnitCore (org.junit.runner) startRunnerWithArgs:68, JUnit4IdeaTestRunner (com.intellij.junit4) startRunnerWithArgs:47, IdeaTestRunner$Repeater (com.intellij.rt.execution.junit) prepareStreamsAndStart:242, JUnitStarter (com.intellij.rt.execution.junit) main:70, JUnitStarter (com.intellij.rt.execution.junit)
-
native stack area
자바 외의 기능들을 사용하기 위한 곳이다. 자바 외의 기능들은 JNI 라는 java native interface 를 통해 실행 된다. JNI 는 대부분 OS와의 통신을 위한 인터페이스를 담당한다고 생각하면 쉽다. JAVA 의 컨셉인 ‘OS 무관 그냥 실행해’ 라는 의미가 가장 생각나는 곳이다. 일반적인 자바 응용 프로그래머는 신경쓰지 않아도 된다.
pc register
스레드가 생성될 때마다 생기는 공간이다. 스레드를 생성한다는 것은 JVM 이 할당된 독립적인 프로세스 안에서 스레드를 생성한다는 의미인데, pc register는 생성되는 스레드들의 명령 히스토리를 관리하기 위한 곳이다.
레퍼런스
-
https://hoonmaro.tistory.com/19
-
https://www.holaxprogramming.com/2013/07/16/java-jvm-runtime-data-area/
-
https://dzone.com/articles/java-8-permgen-metaspace