Back Ground

JVM 메모리 구조 본문

JAVA

JVM 메모리 구조

Back 2017. 3. 9. 21:17


JVM은

응용프로그램이 실행되면, 

JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당 받고 

JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.





1. 메서드 영역(method area)

- 프로그램 실행 중 어떤 클래스가 사용되면, 

  JVM은 해당 클래스의 클래스 파일 (*.class)을 읽어서 분석하여 클래스에 대한 정보(클레스 데이터)를 이곳에 저장한다.

이 때, 그 클래스의 클래스 변수(class variable)도 이 영역에  함께 생성된다.



2. 힙 (heap)

- 인스턴스가 생성되는 공간. 

  프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다.

즉, 인스턴스변수(instance variable)들이 생성되는 공간이다.



3. 호출스택(call stack 또는 execytion stack)

- 호출스택은 메서드의 작업에 필요한 메모리 공간을 제공한다

  메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되며, 

  이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간 결과 등을 저장하는데 사용된다.

그리고 메서드가 작업을 마치면 할당되있던 메모리 공간은 반환되어 비어진다.








1) 호출스택의 변화


각 메서드를 위한 메모리상의 작업공간은 서로 구별되며,



첫 번째로 호출된 메서드를 위한 작업공간이 호출스택의 맨 밑에 마련되고, 






"첫 번째 메서드" 수행 중다른 메서드를 호출하게 되면 , 

"첫 번째 메서드"바로 위두번째 호출된 메서드를 위한 공간이 마련된다.






이때! "첫 번째 메서드"는 수행을 멈추고

       "두 번째 메서드"가 수행되기 시작한다.





"두 번째로 호출된 메서드"가 수행을 마치게 되면

"두 번째메서드"를 위해 제공되었던 호출 스택의 메모리 공간이 반환되며,






"첫 번째메서드"다시 수행을 계속하게 된다.

"첫 번째메서드"수행을 마치면,




역시 제공되었던 메모리 공간이 호출스택에서 제거되며 

호출스택은 완전히 비워지게 된다.



호출스택의 제일 상위에 위치하는 메서드가 현재 실행 중인 메서드이며,

나머지는 대기상태에 있게된다.


따라서, 호출스택을 조사해 보면 메서드 간의 호출관계와 현재 수행중인 메서드가 어느것인지 알 수 있다.





┌ 호출 스택의 특징 ┐

  - 메서드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당 받는다.

  - 메서드가 수행을 마치고나면 사용했던 메모리를 반환하고 스택에 제거된다.

  - 호출스택의 제일 위에 있는 메서드가 현재 실행 중인 메서드이다.

  - 아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드이다.











[ 예제 1 ]

1
2
3
4
5
6
7
8
9
10
11
12
13
class CallStackTest{
    public static void main(String[] args){
        firstMethod(); 
    }
 
    static void first Method(){
        secondMethod();    
    }
    
    static void secondMethod(){
        System.out.println("secondMethod()");
    }
}



main()이 firstMethod()를 호출하고 

firstMethod()는 secondMethod()를 호출한다.



객체를 생성하지 않고도 메서드호출할 수 있으려면, 

메서드 앞에 'static'을 붙여한다.




(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메서드에도 더 이상 수행할 코드가 없으므로 종료되어, 

호출스택은 완전히 비워지게 되고 프로그램은 종료된다. 







[예제 2]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)이 끝났음. 
메서드 시작과 종류 순서를 확인하는 예제






기본형 매개변수와 참조형 매개변수



 기본형 매개변수 - 변수의 값을 읽기만 할 수 있다. (read only)

 참조형 매개변수 - 변수의 값을 읽고 변경 할 수 있다. (read & write)



자바에서는 메서드를 호출할 때 

매개변수로 지정한 값을 

메서드의 매개변수에 복사해서 넘겨준다.


매개변수의 타입이 

기본형(primitive type)일 때는 기본형 값이 복사되겠지만,

참조형(reference type)이면 인스턴스의 주소가 복사된다.


메서드의 매개변수를 

기본형으로 선언하면

단순히 저장된 값만 얻지만,


참조형으로 선언하면

값이 저장된 곳의 주소를 알 수 있기 때문에 

값을 읽어 오는 것은 물론

변경하는 것도 가능하다.





[예제]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package exJava_1;
 
class Data {int x;}
public class exTest_1 {
    
    public static void main(String[] args) {
        Data d = new Data();
        d.x =10;
        System.out.println("main() : x = "+ d.x);
        
        change(d.x);
        System.out.println("After change(d.x)");
        System.out.println("main() :x ="+ d.x );
    }
    
    static void change(int x){
        x = 1000;
        System.out.println("change() : x = "+ x);
    }
    
}


실행결과

change 메서드에서 main메서드부터 

넘겨받은 d.x의 값을 1000으로 변경했는데도

main메서드에서는 d.x의 값이 그대로이다.



그 이유는 

① 

 

 ②

 

 ③

 


① change메서드가 호출되면서 ' d.x '가 change메서드의 매개변수 x에 복사됨

② change메서드에서 x의 값을 1000으로 변경

③ change메서드가 종료되면서 매개변수 x는 스택에서 제거됨


'd.x'의 값이 변경된 것이 아니라, 

change메서드의 매개변수 x의 값이 변경된 것이다.

즉, 원본이 아닌 본사본이 변경된 것이라 원본에는 아무런 영향을 미치지 못한다. 

이처럼 기본형 매개변수는 변수에 저장된 값만 읽을 수만 있을 뿐 변경 할 수는 없다.





-Class로 변수로 주소값으로 값 변경-


[예제]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package exJava_1;
class Data {int x;}
public class exTest_1 {
    
    public static void main(String[] args) {
        Data d = new Data();
        d.x =10;
        System.out.println("main() : x = "+ d.x);
        
        change(d);
        System.out.println("After change(d)");
        System.out.println("main() :x ="+ d.x );
    }
    
    static void change(Data d){
        d.x = 1000;
        System.out.println("change() : x = "+ d.x);
    }
    
}



실행결과


이전 예제와 달리 change메서드를 호출 한 후에 

d.x의 값이 변경되었다.


change메서드의 매개변수가 참조형이라서 값이 아니라 '값이 저장된 주소'를 

change메서드에게 넘겨주었기 때문에 값을 읽어오기는 것 뿐만 아니라 변경하는 것도 가능하다.



① change메서드가 호출되면서 참조변수 d의 값(주소)이 매개변수 d에 복사됨

   이제 매개변수 d에 저장된 주소값으로 x에 접근이 가능

② change메서드에서 매개변수 d로 x의 값을 1000으로 변경

③ change메서드가 종료되면서 매개변수 x는 스택에서 제거됨



[ 즉, 주소값으로 변경을 해야 바뀐다는 말 ]


change메서드의 매개변수를 참조형으로 선언했기 때문에,

x의 값이 아닌 주소가 매개변수 d에 복사되었다.


이제 main메서드의 참조변수 d와 change메서드의 참조변수 d는 같은 객체를 가르키게 된다.


그래서 매개변수 d로 x의 값을 읽는 것과 변경하는 것이 모두 가능한 것이다.





두 예제로 비교해서 알아 보자.

 

[예제]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package aaaa;
 
public class exTest1 {
 
    public static void main(String[] args) {
        int[] x= {10}; //크기가 1인 배열. x[0] = 10
        System.out.println("main() : x = "+ x[0] );
 
        change(x);
        System.out.println("After change(x)");
        System.out.println("main() : x=" + x[0]);
    }
    static void change(int[] x){
        x[0= 1000;
        System.out.println("change() : x="+ x[0]);
    }
 
}
 



[실행결과]


이전의 참조형 매개변수 예제를 Data 클래스의 인스턴스 대신 

길이가 1 인 배열 x를 사용하도록 변경한 것이다.


배열도 객체와 같이 참조변수를 통해 데이터가 저장된 공간에 접근한다.

이전 예제의 Data클래스 타입의 참조변수 d와 같이 변수 x도 

int배열타입의 참조 변수이기 때문에 같은 결과를 얻는다.


임시적으로 간단히 처리 할 때는 별도의 클래스를 선언하는 것보다 

이 처럼 배열을 이용할 수도 있다.





[예제]


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
32
33
34
35
36
package aaaa;
 
public class exTest1 {
 
    public static void main(String[] args) {
        int[] arr = new int[] {3,2,1,6,5,4};
 
        printArr(arr); //배열의 모든 요소를 출력
        sortArr(arr); //배열을 정렬
        printArr(arr); //정렬 후 결과를 출력
        System.out.println("sum="+sumArr(arr)); //배열의 종합을 출력
    }
    static void printArr(int[] arr){ //배열의 모든 요소를 출력
        System.out.print("[");
 
        for(int i:arr) //향상된 for문
        System.out.print(i+",");
        System.out.println("]");
    }
    static int sumArr(int[] arr){ // 배열의 모든 요소의 합을 반환
        int sum =0;
        for(int i=0;i<arr.length;i++)
            sum +=arr[i];
        return sum;
    }
    static void sortArr(int[] arr){ //배열을 오름차순으로 
        for(int i=0;i<arr.length-1;i++)
            for(int j=0;j<arr.length-1-i;j++)
                if(arr[j]>arr[j+1]){
                    int tmp =arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1=tmp;
                }
    }
 
}



[실행결과]

메서드로 배열을 다루는 여러 가지 방법을 보여준 예제이다.

매개변수의 타입이 배열이니까, 참조형 매개변수이다.


그래서 sortArr메서드에서 정렬한 것이 원래 배열에 영향을 미친다.









[예제]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package aaaa;
 
public class ReturnTest {
 
    public static void main(String[] args) {
        ReturnTest r = new ReturnTest();
 
        int result = r.add(3,5);
        System.out.println(result);
 
        int[] result2= {0};//배열을 생성하고 result2[0]의 값을 0으로 초기화
        r.add(3,5,result2);
        System.out.println(result2[0]);
    }
 
    int add(int a, int b) {
        return a+b;
    }
 
    void add(int a, int b, int[] result){
        result[0= a+b;//매개변수로 넘겨받은 배열에 연산결과를 저장
    }
}
 


[실행결과]


이 예제는 반환값이 있는 메서드를 반환값이 없는 메서드로 바꾸는 방법을 보여준다.

앞서 배운 참조형 매개변수를 활용하면 반환값이 없어도 메서드의 실행결과를 얻어 올 수 있다.



1
2
3
int add(int a, int b) {
    return a+b;
}


 

1
2
3
void add(int a, int b, int[] result){
        result[0= a+b;
    }



메서드는 단 하나의 값만 반환 할 수 있지만

이것을 응용하면 여러개의 값을 반환 받는것과 같은 효과를 얻을 수 있다.








참조형 반환타입


매개변수뿐만 아니라 반환타입도 참조형이 될 수 있다.

반환타입이 참조형이라는 것은 

반환하는 값의 타입이 참조형이라는 얘긴데, 

모든 참조형 타입의 값'객체의 주소'이므로 

그저 정수값이 반환되는 것일 뿐 특별할 것이 없다.



[예제]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package aaaa;
 
class Data{int x;}
public class exText4 {
 
    public static void main(String[] args) {
        Data d = new Data();
        d.x=10;
        Data d2=copy(d);
        System.out.println("d.x"+d.x);
        System.out.println("d2.x"+d2.x);
    }
 
     static Data copy(Data d) {
         Data tmp = new Data();
            tmp.x = d.x;
 
            return tmp;
    }
 
}
 


[실행결과]

copy메서드는 새로운 객체를 생성한 다음에,

매개변수로 넘겨받은 객체에 저장된 값을 복사해서 반환한다.


반환하는 값이 Data객체의 주소이므로 반환 타입이 'Data'인 것이다.



1
2
3
4
5
6
 static Data copy(Data d) {
         Data tmp = new Data(); //새로운 객체 tmp를 생성한다.
            tmp.x = d.x;       //d.x의 값을 tmp.x에 복사한다.
 
            return tmp; //복사한 개체의 주소를 반한한다.
    }



이 메서드의 반환타입이 'Data'이므로, 호출결과를 저장하는 변수의 타입 역시 'Data'타입의 참조변수이어야 한다.

1
Data d2=copy(d); // static Data copy(Data d)



copy메서드 내에서 생성한 객체를 mian메서드에서 사용 할 수 있으려면, 
이렇게 새로운 객체의 주소를 반환해줘야 한다.

그렇지 않으면, copy메서드가 종료되면서 새로운 객체의 참조가 사라지기 때문에

더이상 이 객체를 사용할 방법이 없다.


copy메서드가 호출된 직후부터 종료될 때까지의 과정을 단계별로 살펴보면




①copy메서드를 호출하면서 참조변수 d의 값이 매개변수 d에 복사된다.

②새로운 객체를 생성한 다음, d.x에 저장된 값을 tmp.x에 복사한다.

③copy메서드가 종료되면서 반환한 tmp의 값은 참조 변수 d2에 저장된다.

④copy메서드가 종려되어 tmp가 사려졌지만, 

  d2로 새로운 객체를 다룰 수 있다.


즉,

[ "반환타입이 '참조형' 이라는 것은 메서드가 '객체의 주소'를 반환한다는 것을 의미한다. "]





재귀호출 


재귀호출     : 메서드의 내부에서 메서드 자신을 다시 호출하는것
재귀 메서드 : 재귀호출을 하는 메서드

1
2
3
void method(){
    method(); //재귀호출, 메서드 자신을 호출한다.
}
cs

 

어떻게 메서드가 자기 자신을 호출할 수 있는지 의아하겠지만,


메서드 입장에서는 자기 자신을 호출하는 것과 

다른 메서드를 호출하는 것은 차이가 없다.


'메서드 호출'이라는 것이 그저 특정 위치에 

저장되어 있는 명령들을 수행하는 것일 뿐이기 때문이다.


호출된 메서드는 '값에 의한 호출(call by value)'을 통해, 

원래의 값이 아닌 복사된 값으로 작업하기 때문에 

호출한 메서드와 관계없이 독립적인 작업수행이 가능하다.


그런데 위의 코드처럼 오로지 재귀호출뿐이면, 

무한히 자기 자신을 호출하기 때문에 무한반복에 빠지게 된다. 


무한반복문이 조건문과 함께 사용되어야 하는 것처럼,

재귀호출도 조건문이 필수적으로 따라다닌다.



void method(int n){
    if(n==0)
        return;//n의 값이 0일 때, 메서드를 종류한다.
    System.out.println(n);
 
    method(--n); // 재귀호출
}



이 코드는 매게변수 n을 1씩 감소시켜면서 
재귀호출을 하다가 n의 값이 0이 되면 재귀호출을 중단하게 된다.


재귀호출은 반복문과 유사한 점이 많으며, 

대부분의 재귀호출은 반복문으로 작성하는 것이 가능하다.


위의 코드를 반복문으로 작성하면 다음의 오른쪽 코드와 같다.


 

1
2
3
4
5
6
void method(int n){
    if(n==0)   return;
 
    System.out.println(n);
    method(--n); // 재귀호출
}
cs

 

 
1
2
3
4
5
void method(int n){    
    while(n!=0){
      System.out.println(n--);
   }
}
cs


반복문은 그저 같은 문장을 반복해서  수행하는 것이지만,

메서드를 호출하는 것은 반복문보다 몇 가지 과정,

예를 들면 매개변수 복사와 종료 후 복귀할 주소저장 등이 추가로 필요하다 

때문에 반복문보다 재귀호출의  수행시간이 더 오래 걸린다.


그렇다면 '왜? 굳이 반복문대신 재귀호출을 사용할까'.

 └이유 : 재귀호출이 주는 논리적  간결함 때문이다.

몇 겹의 반복문과 조건문으로 복잡하게 작성된 코드가 재귀호출로 작성하면

보다 단순한 구조로 바뀔 수도 있다.


아무리 효율적이라도 알아보기 힘들게 작성하는 것보다 

다소 비효울적이더라도 알아보기 쉽게 작성하는 것이 


논리적 오류가 발생할 확률도 줄어들고 나중에 수정하기도 좋다.


어떤 작업을 반본적으로 처리해야한다면, 

먼저 반복문으로 작성해보고 너무 복잡하면

재귀호출로 간단히 할 수 없는지 고민해볼 필요가 있다.


재귀호출은 비효율적이므로 재귀호출에 드는 비용보다 

재귀호출의 간결함이 주는 이득이 충분히 큰 경우만 사용해야 한다는 것도 잊지말자 



대표적인 재귀호출의 예는 팩토리얼(factorial)을 구하는 것이다.


[팩토리얼]

- 한 숫자가 1이 될 때까지 1씩 감소시켜가면서 계속해서 곱해나가는것


n!(n은 양의 정수)과 같이 표현한다.

예) ' 5! = 5 * 4 * 3 * 2 *  = 120 '이다.


[ 팩토리얼을 수학적 메서드로 표현 ]

 

 f(n) = n * f(n-1) , 단 f(1) = 1





[예제]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package exJava_1;
 
public class FactorialTest {
    public static void main(String[] args) {
        int result = factorial(4); //int result = FactorialTest.factorrial(4);
        System.out.println(result);
        
    }
    static int factorial(int n){
        int result=0;
        
        if (n==1){
            result=1;
        }else{
            result=* factorial(n-1); //다시 메서드 자신을 호출한다.
        }
        return result;
    }
}
 
cs


[실행결과]

24


이 예제는 팩토리얼을 계산하는 메서드를 구현하고 테스트하는 것


factorial메서드가 static메서드이므로 인스턴스를 생성하지 않고 직접 호출 할 수 있다.

그리고 main 메서드와 같은 클래스에 있기 때문에 

static메서드를 호출할 때 클래스이름을 생략하는 것이 가능하다.


그래서  ' FactorialTest.factorial(4) ' 대신 ' factorial(4) '와 같이 하였다.



코드를 좀 더 간단히 하자면,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package exJava_1;
 
public class FactorialTest {
    public static void main(String[] args) {
        int result = factorial(4); //int result = FactorialTest.factorrial(4);
        System.out.println(result);
        
    }
    static int factorial(int n){
        if (n==1return 1;
        return n * factorial(n-1); 
    }
}
 
 
cs


변수에 직접 값을 대입해서 알기쉽게

만일 매개변수 n의 값이 3이라면, n대신 3을 직접 대입해봤다.


 static int factorial(int n){
        if(n==1return 1;
        return n * factorial(n-1); 

  } 

 

  static int factorial(int 3){

        if (3==1return 1;
        return 3 * factorial(3-1); 
    }



'JAVA' 카테고리의 다른 글

자바 Byte /오라클 Byte 크기 다를때 UTF-8은 3Byte  (0) 2017.07.27
ModelAndView  (0) 2017.06.01
JAVA - 배열[]  (0) 2017.02.15
GSON - java 객체(object)를 JSON 표현식으로 변환하는 API  (0) 2016.12.20
오버라이딩/오버로딩  (1) 2016.07.10
Comments