Java(심화) - 참조 타입과 메모리 활용
안녕하세요
Shiny Ocean 입니다 : )
이번 포스팅에서 다루어볼 내용은 자바언어의 참조 타입과 메모리 활용입니다.
자바의 데이터 타입
1. 기본타입 (primitive type)
정수타입 : byte, char, short, int, long
실수타입 : float, double
논리타입 : boolean
2. 참조타임 (reference type) - 객체를 참조하는 타입
배열, 열거, 클래스, 인터페이스
두가지 타입의 가장 큰 차이점은 변수가 가리키는것이 값이냐, 객체의 번지이냐 입니다.
사용자 스택영역 내에서 기본타입의경우 변수에 초기화한 값이 직접적으로 저장되지만 참조타입의 경우 값을 저장해둔 힙영역내 객체의 번지를 참조합니다.
<스택영역>
(int) age = 21
(String) name = 100번지
<힙영역>
100번지 = "kim"
영역이 다르다는것을 표기하기 위함이고 실제로는 힙영역에 객체로 저장되기 때문에 번지수가 문자열값을 가리키지는 않습니다. 100번지 = "kim" 이 아니라 100번지내 String 클래스로 선언된 객체의 여러 값들중 데이터로 사용되는 "kim"이라는 문자열값도 저장되어있음을 의미 합니다.
어쨋든 객체 출력시 toString 메소드를 오버라이딩 하지 않고 출력한다면 기본적으로 해당객체이름@16진수해시값으로 표현됩니다.
간단한 예제를 살펴보겠습니다.
class Human{
int age;
String name;
}
public class Exam {
public static void main(String[] args) {
Human man = new Human();
man.age =21;
man.name = "kim";
System.out.println(man);
}
}
위의 코드에서 Human이라는 객체는 나이와 이름을 가지고 있습니다.
메인문에 나이는 21 이름은 kim이라는 값으로 초기화를 해주고 이 객체를 그대로 출력했습니다.
그럼 결과는 객체이름@해쉬값인 Human@7de26db8가 출력됩니다.
만약 ToString 메소드를 오버라이딩 한다면 원하는 출력값을 얻을수 있도록 변경할수 있으나 man이라는 고유한 변수가 참조하는 객체값이 변경되는것은 아닙니다.
(Human 클래스내에 아래와 같이 오버라이딩 한다면 이름과 나이값이 출력시 확인은 가능)
@Override
public String toString() {
return this.age + ", " + this.name;
}
메모리 사용영역
OS는 응용프로그램 별로 메모리를 할당해주는 것을 우리는 운영체제 카테고리를 통해 다루었습니다
이때 JVM(자바가상머신)은 OS에서 할당받은 메모리 영역(Runtime Data Area)을 세 영역으로 구분합니다.
1. 메소드 영역
JVM을 시작할때 생성되는 영역으로 로딩된 클래스의 바이트 코드가 저장됩니다.
모든스레드가 해당 영역을 공유합니다.
2. 힙 영역
JVM을 시작할때 생성되는 영역으로 객체와 배열을 저장하는 영역으로 사용됩니다.
사용되지 않는객체는 GC(가비지 컬랙터)가 자동으로 제거 합니다.
3. JVM 스택
스레드별로 생성되는 영역으로 메소드를 호출할 때마다 프레임을 스택에 추가합니다(push)
이후 메소드가 종료하면 프레임을 제거(pop) 합니다.
+@ 프레임은 호출되는 프로세스의 크기에 따라 메모리를 일정한 크기로 분할한것으로 이 또한 추가적으로 궁금하다면 운영체제 카테고리의 게시글을 참조해주세요, 여기선 하나의 호출된 메소드를 프레임으로 정의
그렇다면 이제 하나의 자바코드가 실행되는 과정에서 메모리영역은 어떻게 사용되는지 알아보겠습니다.
1. 자바 클래스 코드가 실행됨
2. JVM이 구동되고 메소드 영역과 힙 영역이 생성됨
3. 실행한 코드의 메인클래스를 메소드 영역에 로딩함
4. 메인스레드를 생성함, 이때 JVM 스택도 생성됨
5. 메인 메소드가 호출되고 JVM 스택의 최하단에 메인메소드에 해당하는 프레임이 푸시됨
6. 기본적으로 메인메소드에서 사용하는 매게변수 String[] args를 처리하기 위해 JVM스택의 메인메소드 프레임에 해당 변수(args)와 String 배열 객체의 번지값(힙영역내)를 삽입함
7. 힙영역내에 스트링 배열객체가 삽입됨( 변수 args에 대한), 6번과 동시에 이루어짐
8. 사용자가 정의한 코드에 따라 해당 영역들이 유기적으로 사용됨
7. 모든 스레드가 종료될시 JVM이 종료됨
+@
몇가지 메모리 사용영역과 연관하여 조금만 더 다루어 보겠습니다.
먼저 변수는 {} 블록 단위로 사용되기때문에 조건문또는 반복문 내에서 선언한 변수는 해당 블록을 실행할때 스택영역에 삽입되었다 블록을 빠져나오면 변수도 스택에서 pop됩니다. 따라서 아래의 코드에서 주석을 제거한 이후 실행한다면 b라는 변수가 if블록안에서 사용되었다가 블록을 빠져나올시 스택영역에서 pop되었기 때문에 선언하지 않는 변수를 사용한다는 오류가 나올것입니다.,
public class Exam {
public static void main(String[] args) {
int a =1;
if (a==1) {
int b= 10;
a+=b;
System.out.println(b);
}
System.out.println(a);
//System.out.println(b);
}
}
다음은 객체끼리의 비교연산을 다루어보겠습니다.
흔히 기본타입 변수의 비교연산은 !=, == 을 많이 사용합니다. 하지만 객체끼리의 비교에서는 서로 동일한 객체를 참조하고 있느냐가 같다의 정의가 될수 있습니다. 예를 들어보겠습니다.
class Human{
int age;
String name;
}
public class Exam {
public static void main(String[] args) {
Human man1 = new Human();
man1.age =21;
man1.name = "kim";
Human man2 = new Human();
man2.age =21;
man2.name = "kim";
if(man1==man2) {
System.out.println(true);
}
else {
System.out.println(false);
}
}
}
위의 코드에서 man1 과 man2는 동일한 내용으로 초기화해주었습니다. 아마 toString을 오버라이딩하여 객체의 필드를 찍어보면 거의 비슷할것입니다. 하지만 해당코드의 결과는 당연히 false입니다.
근본적으로 객체변수를 초기화할때 new Human()이라 새로운 객체를 생성하여 힙에 집어 넣었기 때문에 번지수는 정확히 모르지만 분명 다른 객체입니다.
그렇다면 아래의 경우는 어떨까요?
public class Exam {
public static void main(String[] args) {
Human man1 = new Human();
man1.age =21;
man1.name = "kim";
Human man2 = man1;
man2.age =31;
man2.name = "lee";
if(man1==man2) {
System.out.println(true);
}
else {
System.out.println(false);
}
}
}
man2 객체를 선언할때 man1로 초기화했지만 나이와 이름 값은 새롭게 초기화 해봤습니다.
결과는 true 입니다. 이경우 man1의 age와 name 또한 31과 lee로 변경될것입니다. 동일한 객체를 가리키고있는 변수이용하여 필드를 편집했기 때문입니다.