공부했던 자료 정리하는 용도입니다.

재배포, 수정하지 마세요.

 

 

 

 

JVM의 메모리 구조

응용프로그램이 실행되면 JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 용도에 따라 여러 영역으로 나누어 관리한다.

 

1.  메서드 영역(Method Area) 

  : 프로그램 실행 중에 어떤 클래스가 사용되면 JVM은 해당 클래스의 클래스 파일(*. class)을 읽고 분석하여 클래스에 대한 정보(클래스 데이터)를 이곳에 저장한다. 이때 그 클래스의 클래스 변수(class variable)도 이 영역에 함께 생성된다.

 

2.  힙(Heap) 

  : 인스턴스가 생성되는 공간이다. 프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다. 인스턴스 변수들도 이곳에 생성된다.

 

3.  호출 스택(Call Stack 또는 Execution Stack) 

  : 호출스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면 호출 스택에 호출된 메서드를 위한 메모리가 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는 데 사용된다. 메서드가 작업을 마치면 할당되었던 메모리 공간은 반환된다. 각 메서드를 위한 메모리 상의 작업공간은 서로 구별되며 호출된 메서드의 작업공간이 맨 밑에서부터 쌓여나간다. 따라서 호출 스택의 가장 상위에 위치한 메서드가 현재 실행 중인 메서드이며 나머지는 대기상태이다. 호출 스택을 조사해보면 메서드 간의 호출 관계(아래에 있는 메서드가 바로 위의 메서드를 호출한 것)와 현재 수행 중인 메서드가 어느 것인지 알 수 있다.

 

cv는 클래스변수, lv는 지역변수이다.

 

 

 

 

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

기본형 매개변수 : 변수의 값을 읽기만 할 수 있다. (read only)
참조형 매개변수 : 변수의 값을 읽고 변경할 수 있다 (read & write)

 자바에서는 메서드를 호출할 때 매개변수로 지정한 값을 메서드의 매개변수에 복사해서 넘겨준다. 매개변수의 타입이 기본형(Primitive Type) 일 때는 기본형 값이 복사되고 참조형(Reference Type)이면 인스턴스의 주소가 복사된다.

 

package test;

class test{
	public static void main(String[] args) {
		
		int x = 10;

		System.out.println("main() x : " + x);	//바꾸기 전 main()의 x값 출력
		
		change(x);	//change 메서드 호출
		System.out.println("main() x : " + x);	//바꾸고 난 후 main()의 x값 출력
		
	}
	
	static void change(int x) {
		x = 1000;	//x를 1000으로 변경
		System.out.println("change() : x = " + x);	//바꾸고 난 후 change()의 x값 출력
	}
}

 

실행결과_값이 바뀌지 않았음

위의 코드를 실행시켜보면  main 함수의  x 값이 변하지 않는 것을 확인할 수 있다. 매개변수를 받을 때 값을 복사해서 받기 때문이다.  main 함수의  x 값이 변경된 것이 아니라  change( ) 로 복사된  x 의 값이 변경된 것이다. 사본이 변경된 거라 원본인  main 함수의  x 값은 변하지 않는다. 이처럼 기본형 매개변수는 값을 읽어올 수만 있을 뿐 변경할 수는 없다.

 

 

package test;

class Data{ int x; }	//참조형 타입 Data 선언

class test{
	public static void main(String[] args) {
		
		Data d = new Data();	//Data객체 생성
		
		d.x = 10;
		System.out.println("main() x : " + d.x);	//값을 변경하기 전 main의 d.x
		
		change(d);	//change 메서드 호출
		System.out.println("main() x : " + d.x);	//값을 변경하고 난 후 main의 d.x

	}
	
	static void change(Data d) {	//change함수 선언(매개변수를 참조형으로 바꿔줘야함)
		d.x = 1000;	//d.x를 1000으로 변경
		System.out.println("change() : x = " + d.x);	//바꾸고 난 후 change()의 x값 출력
	}
}

 

실행결과_값이 바뀌었다

위의 예제를 바꾼 것이다.  change( ) 메서드를 호출한 후에  d.x 의 값이 변경된 것을 확인할 수 있다.  change( ) 메서드의 매개변수가 참조형이기 때문에 값이 저장된 주소를 넘겨준 것이고 이 주소 값을 이용해 원래의 값을 변경한 것이다.

 

 

 

 

참조형 반환 타입

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

package test;

class Data{ int x; }	//참조형 타입 Data 선언

class test{
	public static void main(String[] args) {
		
		Data d = new Data();	//Data 객체 생성
		d.x = 10;	
		
		Data d2 = copy(d);	//copy()메서드를 호출한 반환값(주소)을 참조형변수인 d2에 저장
		System.out.println("d.x = " + d.x);	
		System.out.println("d.x = " + d2.x);
		
	}
	
	static Data copy(Data d) {	//참조형 타입인 Data를 반환값으로 받는 copy() 메서드 선언
		Data tmp = new Data();	//Data타입의 tmp객체 생성
		tmp.x = d.x;	//d.x가 가리키고 있는 값(주소 X)을 tmp.x에 저장
		
		return tmp;	//tmp의 주소값을 반환
	}
}

 copy( ) 메서드를 이용해서 값을 복사하는 예제이다. 새로운 객체를 생성한 다음에 매개변수로 넘겨받은 객체에 저장된 값(주의! 주소가 X)을 복사한다. 반환하는 값이  Data 객체의 주소이므로 반환 타입이  Data 이다.

이걸 왜 하나 싶은데  main( ) 에서 사용하기 위해서이다.  copy( ) 메서드가 종료되면  copy( ) 메서드 내에 있던 지역변수들도 다 사라지기 때문에  tmp 도 사라지게 된다.  tmp 가 사라지게 되면  tmp 가 참조하고 있는 주소 값도 사라져서 더 이상 새로 생긴 객체를 참조할 수가 없다(참조하는 변수가 없는 객체는 GC(가비지 컬렉터)가 회수한다). 그래서 반환 값으로 주소 값을 넘겨주게 되면  main( ) 에서도 새로 생성된 객체를 다룰 수 있게 되는 것이다!

 

 

 

 

재귀 호출(Recursive Call)

void method(){
	method();	//재귀호출, 자기 자신을 호출한다.
}

  메서드의 내부에서 메서드 자신을 다시 호출하는 것을 재귀 호출이라 하고 재귀 호출을 하는 메서드를 재귀 메서드라고 한다. 호출된 메서드는 '값에 의한 호출(call by value)'을 통해 원래의 값이 아닌 복사된 값으로 작업하기 때문에 호출한 메서드와 관계없이 독립적인 작업 수행이 가능하다. 재귀 호출은 무한히 자기 자신을 호출하기 때문에 무한루프가 된다. 호출한 메서드가 종료되지 않고 계속 쌓여서 스택의 저장 한계를 넘게 되면 스택오버플로우 에러(StackOverflow Error)가 발생한다. 그래서 조건문을 꼭 같이 사용해주어야 한다

 

package test;

class test{
	public static void main(String[] args) {
		int result = factorial(5);
		
		System.out.println(result);
	}
	
	static int factorial(int n) {
		int result = 0;
		
		if(n == 1) {	// 0!==1 이므로 if문을 사용한다.
			result = 1;
		}else {	//0!이 아닐경우
			result = n * factorial(n - 1);	//다시 자기자신을 호출한다
		}
		
		return result;
	}
}

재귀 호출의 대표적인 예로 팩토리얼을 구하는 프로그램이 있다. 입력받은 값( n )에서  1 을뺀 값을 매개 변수로 해서 자기 자신을 호출하는 과정을 반복하다  n  1 이되면  result 값으로  1 을 반환한다. 그러면  (((((1) * 2) *3) *4) *5)  이런 식으로 값이 나오게 된다.

 

반복문이 그냥 같은 문장을 반복해서 수행하는 것이라면 메서드를 호출하는 것은 여러 가지 과정(매개변수의 복사와 종료 후 복귀할 주소 저장 등)을 더 거쳐야 되기 때문에 반복문보다 재귀 호출의 수행 시간이 더 오래 걸린다. 그럼에도 논리적 간결함 때문에 재귀 호출을 사용하는 경우가 있다.

 

 

 

 

클래스 메서드(Static 메서드)와 인스턴스 메서드

인스턴스메서드 : 메서드의 작업을 수행하는데 인스턴스 변수를 필요로하는 메서드
클래스메서드(static 메서드) : 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드

멤버 변수는 클래스 영역에서 선언된 변수를 멤버 변수라고한다. 멤버변수 중에서  static 이 붙은걸 클래스 변수(static 변수)라 하고  static 이 붙지 않은 것들을 인스턴스 변수라고 한다. 따라서 멤버 변수는 인스턴스 변수와 클래스 변수를 모두 통칭하는 말이다.  

 

 

   클래스를 설계할 때 참고할만한 기준 

  1. 클래스를 설계할 때 클래스의 멤버 변수 중 모든 인스턴스에 공통된 값을 유지해야 하는 것이 있는지 살펴보고, 있다면 static을 붙여준다
  2. 클래스 메서드(static 메서드)는 인스턴스 변수를 사용할 수 없다.
    : 클래스 변수나 클래스 메서드는 클래스가 메모리에 올라갈 때 생성되므로 인스턴스의 생성 없이 호출이 가능한 반면에 인스턴스 변수는 인스턴스를 생성해야만 사용할 수 있기 때문에 클래스 메서드가 호출되었을 때 인스턴스가 존재하지 않을 수도 있다. 그래서 클래스 메서드에서 인스턴스 변수를 사용하는 것을 금지한다. 

     반면에 인스턴스 변수나 인스턴스 메서드에서는 static이 붙은 멤버들을 언제나 사용할 수 있다. 인스턴스 변수가 존재한다는 것은 static변수가 이미 메모리에 존재한다는 것을 의미하기 때문이다.

  3. 작성한 메서드 중에서 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드에는 static을 붙일 것을 고려한다.
    : 메서드 호출시간이 짧아지므로 성능이 향상된다. 인스턴스 메서드는 실행 시 호출되어야 할 메서드를 찾는 과정이 추가적으로 필요하기 때문에 시간이 더 걸린다.

 

 

 

 

클래스 멤버와 인스턴스 멤버 간의 참조와 호출

 같은 클래스에 속한 멤버들 간에는 별도의 인스턴스를 생성하지 않고도 서로 참조하거나 호출하는 것이 가능하다. 단, 클래스 멤버가 인스턴스 멤버를 참조하거나 호출하고자 하는 경우에는 인스턴스를 생성해야 한다. 인스턴스 멤버가 존재하는 시점에 클래스 멤버는 항상 존재하지만, 클래스 멤버가 존재하는 시점에 인스턴스 멤버가 존재하지 않을 수도 있기 때문이다. 

 

  같은 클래스 내에서 클래스 멤버가 인스턴스 멤버를 참조하거나 호출해야 하는 경우는 드물다. 만일 그런 경우가 발생한다면, 인스턴스 메서드로 작성해야 할 메서드를 클래스 메서드로 한 것은 아닌지 한번 더 생각해봐야 한다.

 

 

 

+ Recent posts