
[Java] 자바 메모리 구조와 변수 타입
zl존석동
·2021. 12. 22. 23:23
다룰내용
JVM Memory
Static
Primitive Type , Reference Type
자바 프로그램이 구동되면 OS 로부터 JVM 이 메모리를 할당받게 되고
할당받은 메모리를 용도에 맞게 여러 영역으로 나누어서 관리를 하게 된다.
메모리는 컴퓨터에서 한정되어있는 자원이기 때문에 효율적으로 사용되고 관리되어야 할 것이다.
JVM 메모리 구조를 알아보며 어떤 메모리 영역이 있고 그 영역에 자바의 어떤 것들이 어떻게 관리되는지 자바 변수 타입과도 연관지어 겉을 핥아 보는 시간을 가져보자.
JVM Memory
Method(Class) Area(메소드 영역)
클래스 로더가 로드한 클래스의 메타 데이터가 저장된다.
예를 들면 클래스의 멤버변수 이름, 타입, 접근제어자, 메소드 등등..
클래스에 있는 static 변수도 단 한번 실행된다.
클래스 로더에 의해 동적으로 로드된 클래스의 정보는 언제든지 객체로 힙에 생성될 수 있도록 프로그램 종료 전 까지 상주해있다.
따라서 당연하게도 모든 스레드가 공유할 수 있는 메모리 영역이다.
Heap Area(힙 영역)
new 키워드로 생성되는 객체, 배열이 동적으로 생성되는 영역으로 메소드 영역에 상주해있는 클래스로만 생성이 가능하다.
힙의 더 이상 참조되지 않는 의미 없는 메모리 회수 권한은 GC(가비지 컬렉터)에게만 있다.
GC는 프로그램에서 더 이상 사용되지 않는다고 판단되는 객체를 메모리 영역에서 제거한다.
힙 영역의 자원은 모든 스레드에서 공유된다.
Stack Area(스택 영역)
메소드 호출 시 해당 메소드만을 위한 프레임이 생성되고 스택에 쌓인다.
메소드에서 필요한 파라미터, 로컬변수, 리턴, 연산 값 등을 임시로 저장한다.
메소드 수행이 정상적으로 완료되거나 예외가 발생하면 프레임이 스택에서 pop 되면서 소멸하게 된다.
스레드마다 스택을 영역을 만들게 되고 스레드가 종료되면 스택도 제거된다.
기본타입 변수의 경우 변수와 데이터 값이 함께 스택에 할당되고 객체를 생성했다고 할 때 생성된 객체가 힙에 저장된다면 그 객체의 주소값을 가지는 변수는 스택에 저장되게 된다.
PC Register
PC Registers는 Thread가 생성될 때 마다 생기는 공간으로 Thread가 어떠한 명령을 실행하게 될지에 대한 부분을 기록을 한다.
Native Method Stack
자바 외 언어로 작성한 네이티브 코드를 위한 메모리 영역이다.
자바 메모리구조에 대해 간단히 살펴보았는데
우리가 자바를 통해 메모리 사용을 컨트롤할 수 있는 부분은 static 키워드의 활용과 변수의 타입이 아닐까라는 생각이 듭니다.
static 키워드와 변수타입에 대해 겉을 핥다 보면 자바 프로그램 메모리 관리에 대한 인사이트가 생겨나지 않을까요??
Static
변수와 메소드에 사용하는 키워드로 static 변수는 클래스가 메모리에 올라갈 때 자동적으로 생성되게 된다.
일반적인 클래스에 대한 객체 생성시의 인스턴스 변수처럼 힙에 할당되어 사용되다가 GC 되는 것이 아니라
메소드(클래스) 메모리 영역에 한번 생성되면 계속 상주하게 되는 것이다.
즉 객체 소속의 변수가 아니라 클래스 소속의 변수라 객체로 초기화 하지 않고도 사용할 수 있다.
객체로 초기화 하지 않고도 사용할 수 있다 라는 말은 곧
프로그램 내에서 공통으로 사용될 수 있다 라는 말이 되기도 한다.
사용이 잦으나 공통된 것을 사용할 때 재사용성을 높일 수 있겠지만
무분별하게 남발하면 프로그램 내에서 상주하는 메모리가 많아져서 오히려 비효율적이게 될 것이다.
공통으로 사용될 수 있다라는 말과 객체 초기화 없이 사용할 수 있다는게 무슨 말인지 간단한 예시로 살펴보자.
다음과 같은 People 클래스가 있다고 하자.
class People {
// static 변수
private static int id = 0;
private int instanceId = 0;
private String name;
public People(String name) {
this.name = name;
id++;
this.instanceId++;
}
@Override
public String toString() {
return "이름 : " + name + " id : " + id + " instancId : " + instanceId;
}
// static 메소드
public static void printStatic() {
System.out.println("인류는 위대하다");
}
}
이 코드를 수행했을 때 어떤 결과가 나올 것 같습니까?
People p1 = new People("김갑환");
System.out.println(p1);
People p2 = new People("최번개");
System.out.println(p2);
// static 메소드를 객체 생성 없이 사용해보자
People.printStatic();
공통사용?
id 와 instanceId 의 기본 값을 모두 0으로 지정하였고 생성자에서 객체 생성 시 모두 +1 이 되도록 설정을 했다.
클래스의 멤버 변수인 instanceId 의 경우 객체 생성 시점에 힙 에 저장되기 떄문에 김갑환 최번개를 생성했을 때의 instanceId 는 모두 다르기 떄문에 초기화 할 때 마다 기본값 0에서 생성자를 거쳐 1이라는 값을 가지게 되는 것이다.
하지만 Static 키워드가 붙은 id 의 경우는 클래스가 메모리에 올라갈 때 메소드 메모리 영역 에 상주하게 된다.
그런 다음 김갑환을 생성할 때 생성자를 통해 상주해 있는 id 의 값(0) 에 + 1 이 되고
최번개를 생성할 때 또 생성자를 통해 상주해 있는 값(1) 에 + 1 이 되어 2를 보여주는 것이다.
객체 생성 없이 사용?
People.printStatic(); 처럼 클래스명을 통해서 바로 메소드에 접근해 사용할 수 있다.
문자열을 기본타입으로 변환해주는 parseXXX("문자열"); 를 생각해보자
분명히 Integer , Double 등등.. Wrapper 클래스 이름에 바로 접근해서 사용했다.!
Integer.parseInt("1323");
Static 메소드라 그런 것이였다. Integer 클래스의 parseInt(String s) 메소드를 살펴보자
public final class Integer extends Number implements Comparable<Integer> {
// ...
// ... 생략
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
}
// ...
}
Primitive Type , Reference Type
Primitive Type(기본 타입 변수)
정수, 실수, 문자, 논리 데이터의 실제 값 을 저장하는 타입으로 Stack 에 저장된다.
기본 값이 존재하기 때문에 Null 값은 존재하지 않는다.
byte , short , int , long , float , double , char , boolean
Reference Type(참조 타입 변수)
객체의 주소를 참조(저장) 하는 타입으로 힙 영역에 저장된다.
빈 객체를 뜻하는 Null 값이 존재한다.
String, Array, Class, Interface, Enum 등
객체가 초기화되면 힙에는 객체의 주소와 정보가 들어있고 선언한 로컬변수는 주소 값을 가지게 된다.
// 기본 타입
int a = 1;
int b = 1;
boolean c = a==b;
// c = true;
// 참조 타입
String A = new String("A");
String B = new String("A");
// false
System.out.println(A==B);
// true
System.out.println(A.equals(B));
'==' 연산자의 경우 기본형 타입에서는 둘의 그 '값' 자체를 비교했고 따라서 할당된 값이 같으면 true , 아니면 false 였다.
하지만 참조형 타입에서는 둘의 '주소'를 비교하게 된다. 주소를 가지고 있으니까
그래서 내부 값의 비교를 위해 equals() 라는 메소드를 사용하게 된다.
물론 String, Integer 등등은 Object 클래스의 equals 를 오버라이딩 하여 값을 비교하게끔 만들어져서 그런 것이지만 쉽게쉽게 값을 비교한다 라고 생각을 해두면 될 것 같다.
참조 타입에서 변수가 주소를 가리킨다면.. 같은 주소를 가리키는 다른 두 개의 변수는 서로 종속적이지 않을까?
간단한 코드 디버깅으로 확인해보자
People p1 = new People("김갑환");
System.out.println();
People p2 = p1;
디버깅 해보면 People 클래스의 객체인 p1 과 p2 모두 32 라는 id 를 가지고 있음을 확인할 수 있다.
여기서 p1의 이름을 setter 를 통해 바꿔보자. 과연 같은 id를 가진 p2 는 변화가 있을까?
p1.setName("최번개");
//return true;
System.out.println(p1.hashCode() == p2.hashCode());
분명 p1 의 이름 속성만 바꿨는데 p2 의 이름까지 최번개로 바뀐 것을 알 수 있다.
변수는 단지 주소를 가리킬 뿐이고 실제 객체는 힙 영역에 존재하기 때문에 객체의 어떤 변화를 공유하게 되는 것이다.
공유되어진다는 것은 어떤 면에서 굉장히 효율적일 수 있지만 어떤 면에서는 굉장히 위험할 수도 있지 않을까라는 생각을 하게 된다.
마무리
자바로 프로그래밍을 할 때 메모리 구조가 어떻게 되어 있고 어떤 것들이 어디에 들어가 어떻게 처리되는지 기본적으로라도 알고 있어야 메모리 관리 이전에 이상한 치명적인 실수를 안 하지 않을까 싶다.
다음에 왜 자바가 Call By Value 인지, GC 가 무엇이고 어떻게 동작하는지에 대해 왠지 알아봐야 할 것만 같은 느낌이 든다.
ref
'Java' 카테고리의 다른 글
[Java] Abstract , Interface (0) | 2022.01.09 |
---|---|
[Java] 람다식이란 (0) | 2022.01.06 |
[Java] 중첩 클래스 (0) | 2022.01.05 |
[Java] 자바는 Call by Value (feat. C++) (0) | 2022.01.04 |
[Java] Java 의 동작 (0) | 2021.12.20 |