'JVM'에 해당되는 글 1건
- 2009.01.15 JVM 메모리 구조 및 설명
2009. 1. 15. 11:16
JVM 메모리 구조 및 설명
2009. 1. 15. 11:16 in Java
JVM의 메모리 구조를 좀 알아둘 필요가 생겨서 찾아봤다.
응용프로그램이 실행되면, JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.
그 중 3가지 주요영역(Method Area, 호출스택, Heap)에 대해서 알아보도록 하자.
[참고] cv는 클래스변수, lv는 지역변수, iv는 인스턴스변수를 뜻한다.
1. 메소드영역(Method Area)
- 프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스파일(*.class)을 읽어서 분석하여 클래스에 대한 정보(클래스 데이타)를 Method Area에 저장한다. 이 때, 그 클래스의 클래스변수(class variable)도 이 영역에 함께 생성된다.
2. 힙(Heap)
- 인스턴스가 생성되는 공간. 프로그램 실행 중 생성되는 인스턴스는 모두 이 곳에 생성된다. 즉, 인스턴스변수(instance variable)들이 생성되는 공간이다. 3. 호출스택(Call Stack 또는 Execution Stack)
호출스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다. 그리고, 메서드가 작업을 마치게 되면, 할당되었던 메모리공간은 반환되어 비워진다. 각 메서드를 위한 메모리상의 작업공간은 서로 구별되며, 첫 번째로 호출된 메서드를 위한 작업공간이 호출스택의 맨 밑에 마련되고, 첫 번째 메서드 수행중에 다른 메서드를 호출하게 되면, 첫 번째 메서드의 바로 위에 두 번째로 호출된 메서드를 위한 공간이 마련된다.
이 때 첫 번째 메서드는 수행을 멈추고, 두 번째 메서드가 수행되기 시작한다. 두 번째로 호출된 메서드가 수행을 마치게 되면, 두 번째 메서드를 위해 제공되었던 호출스택의 메모리공간이 반환되며, 첫 번째 메서드는 다시 수행을 계속하게 된다. 첫 번째 메서드가 수행을 마치면, 역시 제공되었던 메모리 공간이 호출스택에서 제거되며 호출스택은 완전히 비워지게 된다.
호출스택의 제일 상위에 위치하는 메서드가 현재 실행 중인 메서드이며, 나머지는 대기상태에 있게 된다.
따라서, 호출스택을 조사해 보면 메서드 간의 호출관계와 현재 수행중인 메서드가 어느 것인지 알 수 있다.
호출스택의 특징을 요약해보면 다음과 같다.
- 언제나 호출스택의 제일 위에 있는 메서드가 현재 실행 중인 메서드이다. - 아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드이다. |
반환타입(return type)이 있는 메서드는 종료되면서 결과값을 자신을 호출한 메서드(caller)에게 반환한다. 대기상태에 있던 호출한 메서드(caller)는 넘겨받은 반환값으로 수행을 계속 진행하게 된다.
[예제6-6] CallStackTest.java |
secondMethod() class CallStackTest { public static void main(String[] args) { firstMethod(); } static void firstMethod() { secondMethod(); } static void secondMethod() { System.out.println("secondMethod()"); } } |
[실행결과] |
위의 예제를 실행시켰을 때, 프로그램이 수행되는 동안 호출스택의 변화를 그림과 함께 살펴보도록 하자
(1)~(2) 위의 예제를 컴파일한 후 실행시키면, JVM에 의해서 main메서드가 호출됨으로써 프로그램이 시작된다. 이때, 호출스택에는 main메서드를 위한 메모리공간이 할당되고 main메서드의 코드가 수행되기 시작한다.
(3) main메서드에서 firstMethod()를 호출한 상태이다. 아직 main메서드가 끝난 것은 아니므로 main메서드는 호출스택에 대기상태로 남아있고 firstMethod()의 수행이 시작된다.
(4) firstMethod()에서 다시 secondMethod()를 호출했다. firstMethod()는 secondMethod()가 수행을 마칠 때까지 대기상태에 있게 된다. seoundMethod()가 수행을 마쳐야 firstMethod()의 나머지 문장들을 수행할 수 있기 때문이다.
(5) secondMethod()에서 println메서드를 호출했다. 이때, println메서드에 의해서 화면에 "secondMethod()"가 출력된다.
(6) println메서드의 수행이 완료되어 호출스택에서 사라지고 자신을 호출한 secondMethod()로 되돌아간다. 대기 중이던 secondMethod()는 println메서드를 호출한 이후부터 수행을 재개한다.
(7) secondMethod()에 더 이상 수행할 코드가 없으므로 종료되고, 자신을 호출한 firstMethod()로 돌아간다.
(8) firstMethod()에도 더 이상 수행할 코드가 없으므로 종료되고, 자신을 호출한 main메서드로 돌아간다.
(9) main메서드에도 더 이상 수행할 코드가 없으므로 종료되어, 호출스택은 완전히 비워지게 되고 프로그램은 종료된다.
[예제6-7] CallStackTest2.java |
class CallStackTest2 { public static void main(String[] args) { System.out.println("main(String[] args)이 시작되었음."); firstMethod(); System.out.println("main(String[] args)이 끝났음."); } static void firstMethod() { System.out.println("firstMethod()이 시작되었음."); secondMethod(); System.out.println("firstMethod()이 끝났음."); } static void secondMethod() { System.out.println("secondMethod()이 시작되었음."); System.out.println("secondMethod()이 끝났음."); } } |
[실행결과] |
main(String[] args)이 시작되었음. firstMethod()이 시작되었음. secondMethod()이 시작되었음. secondMethod()이 끝났음. firstMethod()이 끝났음. main(String[] args)이 끝났음. |
/// 또다른 아티클..
Java Virtual Machine의 메모리 관리
전통적인 C언어나 C++언어와는 달리 자바에서는 메모리 관리는 프로그래머가 일일이 하지 않고 JVM에서 알아서 관리 해 줍니다.(물론 닷넷의 경우엔 자바와 비슷한 방법으로 하지만)
메모리 관리란 크게 보면 생성/소멸(삭제, 반납)의 과정으로 생각 할 수 있는데 우리가 자바 프로그램에서 Hello h = new Hello(“방가방가”) 라고 하든지 아님 int i=10; 이라고 할 때 메모리의 일정 부분을 할당 받게 되는 것입니다. 메모리에서 할당이 필요해지면 JVM은 변수에 대해 실제의 내용을 포인터로써 관리를 해주게 됩니다. JVM이 모든 객체의 레퍼런스를(참조) 카운트하고 관리하기 때문에 메모리 영역의 충돌이나 삭제 하지 않은 메모리 때문에 memory leak등이 발생 할 가능성이 거의 없어 졌습니다.
HOT SPOT이전의 JVM(JIT기반, JDK1.3 이전)에서는 메모리에 대한 간접 참조 방식을 이용하였는데 아시는 것 처럼 간접 방식이므로 실제 내용을 찾아 가기 위해서는 두번이상의 참조가 일어나게 됩니다. 물론 조금은 느려질 소지가 있었겠지요~ 이 경우엔 오히려 Garbage Collection시에는 참조만 지우면 되므로 오히려 간단했다고 볼 수 있습니다.
간접 참조란 힙에 객체의 내용이 있다면 중간에 그곳을 가리키는 메모리 영역이 있고(핸들 메모리) 스택에 있는 변수가 핸들 메모리를 참조해서 실제 힙의 주소를 얻은 후 힙의 실제 내용을 참조하게 되는 방식 입니다. 반면 직접 참조의 경우 스택에 있는 변수가 힙메모리의 실제 주소를 가지고 있어 직접 참조 하게 되는 방식 입니다. 그러므로 자주 참조되는 변수나 객체가 있다면 직접 참조 방식이 훨씬 뛰어난 성능을 보입니다.
반면에 가비지컬렉션의 경우(메모리를 지우거나 참조값을 바꾸는 경우) 직접 참조 방식에서는 값이 없어지거나 사라져야 하는 변수에 대해 일일이 포인터를 지워줘야 합니다. 반면 간접 참조 방식의 경우 핸들 메모리만 수정을 해주면 되므로 오히려 간단해 지는 것입니다.
Virtual Machine에서는 스택과 힙 메모리를 사용하며 스택에는 프로그램에서 사용하는 변수, 함수명등이 수행 순서에 맞게 적재 되어 있고 힙 메모리에는 실제 스택에 있는 변수나 함수의 내용이 적재 됩니다. 즉 스택에는 실제 값이 존재하는 곳의 포인터를 가지므로 크기가 크지 않으나 힙의 경우 사용량이 커지게 됩니다. 만약 힙 메모리를 다써버린 다면 java.lang.OutOfMemory 오류가 발생 할겁니다.
이번에는 메모리의 해제에 대해 보도록 하겠습니다. 아까 위에서 만든 객체 참조변수에 대해 h = null; 이라고 하는 경우나 어떤 메소드 안에 쓰인 변수가 메소드의 실행이 종료하게 되는 경우에 메모리에서 해제 될 것 입니다.
JVM의 힙메모리에는 프로그램 실행시 생성된 여러 값들이 존재 합니다. 가비컬렉터가 하는 일은 이러한 힙 메모리중 사용하지 않는 메모리를 반환 시키는 역할을 하는 것입니다. 위의 경우 처럼 h = null; 이라고 명시적으로 지정 하는 경우 객체 헤더에 존재하는 GC(Garbage Collector) 필드가 값이 설정 됩니다. 이렇게 지정 된 객체에 대해 우선적으로 메모리에서 삭제를 하게 됩니다.
반면 스코프를 벗어나는 경우 Local 변수는 다음과 같은 조건에 따라 삭제 여부를 판단하게 됩니다. 스택 메모리에 있는 변수 중 해당 객체에 대한 참조가 있는 경우 또는 객체가 메모리에 있다는 이야기는 다음에 있을 확률이 높다는 판단(이게 LRU방식 맞나여??)을 JVM에서 해(일반적으로 프로그래밍에서 사용된 객체의 95% 이상이 생성된 후 곧 삭제된다는 가정 하에 아직까지도 있는 객체이므로 필히 중요한 것 일꺼야 하는 기술을 Generation Copying Collection이라 합니다) 가급적 가비지 컬렉션을 수행 하지 않습니다. 또는 정적 혹은 현재 스코프의 객체가 해당 객체를 참고 하고 있는 경우, JVM내부에서 사용하는 네이티브 메소드에 사용되는 객체에 해당 하는 경우 등은 가비지 컬렉션에서 제외를 하며 기타의 경우엔 메모리에서 삭제를 하게 됩니다. 대부분의 프로그래밍 언어에서 사용된 객체는 거의 금방 소멸 된다는 것 때문에 Java에서는 메모리를 두가지 영역으로 메모리를 나누는데 Young 영역과 Old 영역으로 나눕니다. Young 영역은 생긴지 얼마 안된 객체들을 저장하는 장소이고, Old영역은 생성된지 오래된 객체를 저장하는 장소 입니다. 각 영역의 성격이 다른 만큼 GC의 방법 또한 다릅니다. 또한 Perm 영역이라고 불리는 곳이 있는데 이곳에는 클래스나 메소드의 실행 코드가 저장 됩니다. 그래서 GC가 일어나는 곳은 old, new 영역이며 Perm영역은 GC가 수행 되지 않습니다.(소스 코드가 있으므로)
JDK1.3 이상에서 탑재된 HOT SPOT VM의 경우(병목 현상이 일어날 확률이 높은 부분을 먼저 컴파일) 가비지 컬렉션에 따른 성능 저하를 막기 위해 Generation Copying Collection 기술을 이용하여 가비지 컬렉션 대상을 줄였으며 Incremetal Pauseless 기법을 이용하여 가비지컬렉션 대상을 여러 개의 작은 그룹으로 나누어 확실히 삭제 가능한 객체부터 지워나가는 방식을 채택 했습니다. Incremetal Pauseless 기법에서는 1차 가비지 컬렉션 대상은 객체의 헤더에 GC 필드가설정된 것이고 2차 대상은 위의 4가지 유형에 속하지 않은 것 입니다. 2차 대상의 경우 일단 CG필드를 설정 하고 다음번 가비지 컬렉션 주기에 삭제 되는 것 입니다.