본문 바로가기
코딩

[ Java ] 실전자바-기본편 7. 자바 메모리 구조와 static

by 모두의 아카이브 2024. 2. 15.
반응형

자바 메모리 구조

출처: 김영한의 실전자바-기본편

  • 메서드 영역: 클래스 정보를 보관한다. 이 클래스 정보가 붕어빵 틀이다.
  • 스택 영역: 실제 프로그램이 실행되는 영역이다. 메서드를 실행할 때 마다 하나씩 쌓인다.
  • 힙 영역: 객체(인스턴스)가 생성되는 영역이다. new 명령어를 사용하면 이 영역을 사용한다. 쉽게 이야기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간이다. 참고로 배열도 이 영역에 생성된다.

출처: 김영한의 실전자바-기본편

  • 메서드 영역: 프로그램을 실행하는데 필요한 공통 데이터를 관리한다. 이 영역은 프로그램의 모든 영역에서 공유한다.
    • 클래스 정보: 클래스의 실행코드(바이트 코드), 필드 , 메서드와 생성자 코드등 모든 실행 코드가 존재한다.
    • static 영역: static변수들을 보관한다.
    • 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다. 예를 들어서 프로그램에 "hello" 
      라는 리터널 문자가 있으면 문자를 공통으로 묶어서 관리한다. 이 외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리한다.
  • 스택 영역: 자바 실행 시, 하나의 실행 스택이 생성된다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
    • 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.
  • 힙 영역: 객체(인스턴스)와 배열이 생성되는 영역이다., 가비지 컬렉션이 이루어지는 주요 영역이며,더 이상 참조되지 않는 객체는 GC에 의해 제거된다.

인스턴스를 호출하면 메서드 영역에 있는 코드를 불러서 수행

 

스택과 큐 자료 구조

스택 구조

출처: 김영한의 실전자바-기본편

 

해당 사진처럼 뚜껑이 위쪽만 열려있어서 위쪽으로 블럭을 넣고, 빼는걸 해야한다.

따라서 1(넣기) -> 2(넣기) -> 3(넣기) -> 3(빼기) -> 2(빼기) -> 1(빼기)

 

이렇게 나중에 넣은 것이 가장 먼저 나오는 것을 후입 선출이라 하고, 이런 자료구조를 스택이라 한다.

 

출처: 김영한의 실전자바-기본편

 

1(넣기) -> 2(넣기) -> 3(넣기) -> 1(빼기) -> 2(빼기) -> 3(빼기)

 

가장 먼저 넣은 것이 가장 먼저 나오는 것을 선입 선출이라 한다. 이런 자료구조를 라고 한다.

 

스택 영역

package memory;
public class JavaMemoryMain1 {
 public static void main(String[] args) {
     System.out.println("main start");
     method1(10);
     System.out.println("main end");
 }
 static void method1(int m1) {
     System.out.println("method1 start");
     int cal = m1 * 2;
     method2(cal);
     System.out.println("method1 end");
 }
 static void method2(int m2) {
     System.out.println("method2 start");
     System.out.println("method2 end");
 }
}

 

실행결과

main start
method1 start
method2 start
method2 end
method1 end
main end

 

  • 처음 자바 프로그램을 실행하면 main()을 실행한다. 이때 main()을 위한 스택 프레임이 하나 생성된다.
    • main() 스택 프레임은 내부에 args라는 매개변수를 가진다. 
  • main()은  method1()을 호출한다. method1() 스택 프레임이 생성된다.
    • method1()는 m1,cal 지역변수(매개변수 포함)를 가지므로 해당 지역 변수들이 스택 프레임에 포함된다.
  • method1() 은 method2() 를 호출한다. method2() 스택 프레임이 생성된다. method2() 는 m2 지역 변 수(매개변수 포함)를 가지므로 해당 지역 변수가 스택 프레임에 포함된다.

  • method2()가 종료된다. 이때 method2() 스택 프레임이 제거되고, 매개변수 m2도 제거된다. method2() 스택 프레임이 제거 되었으므로 프로그램은 method1()로 돌아간다. 이때  method1()에서 method2()를 호출한 지점으로 돌아간다.
  • method1()이 종료된다. 이때 method1()스택 프레임이 제거되고, 지역변수(매개변수 포함)m1, cal도 제거된다.
    프로그램은 main()으로 돌아간다.
  • main()이 종료된다. 더 이상 호출할 메서드가 없고, 스택 프레임도 완전히 비워졌다. 자바는 프로그램을 정리하고 
    종료한다.

정리

  • 자바는 스택 영역을 사용해서 메서드 호출과 지역변수(매개변수 포함)을 관리한다.
  • 메서드를 계속 호출하면 스택 프레임이 계속 쌓인다.
  • 지역변수(매개변수 포함)는 스택 영역에서 관리한다.
  • 스택 프레임이 종료되면 지역 변수도 함께 제거된다.
  • 스택 프레임이 모두 제거되면 프로그램도 종료된다.

스택 영역과 힙 영역

package memory;
public class Data {
 private int value;
 public Data(int value) {
 this.value = value;
 }
 public int getValue() {
 return value;
 }
}
package memory;
public class JavaMemoryMain2 {
 public static void main(String[] args) {
     System.out.println("main start");
     method1();
     System.out.println("main end");
 }
 static void method1() {
     System.out.println("method1 start");
     Data data1 = new Data(10);
     method2(data1);
     System.out.println("method1 end");
 }
 static void method2(Data data2) {
     System.out.println("method2 start");
     System.out.println("data.value=" + data2.getValue());
     System.out.println("method2 end");
 }
}

 

실행결과

main start
method1 start
method2 start
data.value=10
method2 end
method1 end
main end

 

순서대로 살펴보면 

 

처음 main() 메서드를 실행해서 main() 스택 프레임이 생성된다.

 

  • main()에서 method1()을 실행해서 스택 프레임이 생성된다.
  • method1()은 지역변수로 Data data1을 가지고  있어, 이 지역변수도 스택 프레임에 포함된다.
  • method1()은 new Data(10)를 사용해서 힙 영역에 Data인스턴스를 생성한다. 그리고 참조값을 data1에 보관

  • method1()은 method2()를 호출하면서 Data data2 매개변수에 x001  참조값을 넘긴다.
  • 이제 method1()에 있는 data1과 method2()에 있는 data2 지역변수는 둘다 같은 인스턴스를 참조한다.

method2()의 스택 프레임이 제거되면서 매개변수 data2도 함께 제거된다.

 

method1()의 스택프레임이 제거되면서 매개변수 data1도 함께 제거된다.

 

  • method1()이 종료된 직후 지역변수 data1도 함께 제거
  • data1을 참조하는 곳이 없으므로 GC가 찾아서 메모리에서 제거

지역 변수는 스택영역에, 객체(인스턴스)는 힙 영역에서 관리

 static 변수

특정 클래스를 통해서 생성된 객체의 수를 세는 프로그램을 만들어보자

package static1;
public class Data1 {
 public String name;
 public int count;
 public Data1(String name) {
     this.name = name;
     count++;
 }
}
package static1;
public class DataCountMain1 {
 public static void main(String[] args) {
 Data1 data1 = new Data1("A");
 System.out.println("A count=" + data1.count);
 Data1 data2 = new Data1("B");
 System.out.println("B count=" + data2.count);
 Data1 data3 = new Data1("C");
 System.out.println("C count=" + data3.count);
 }
}

 

실행결과

A count=1
B count=1
C count=1

해당 프로그램이 개수가 다 1인 이유는 객체를 생성할때마다 Data1 인스턴스는 새로 만들어진다. 그리고 포함된
count변수도 새로 만들어진다.

해당 사진처럼 A,B,C 모두 이전 인스턴스와는 관계없는 새로운 인스턴스를 생성하고, 이 인스턴스의 count 값은 0으로 

초기화된다. 따라서 count의 값은 1이다.

 

이런 문제를 해결하기 위해 특정 클래스에서 공용으로 함께 사용할 수 있는 변수를 만들면 편리할 건데 이때
static키워드를 사용하면 만들수 있다.

 

package static1;
public class Data3 {
 public String name;
 public static int count; //static
 public Data3(String name) {
     this.name = name;
     count++;
 }
}

멤버 변수에 static을 붙이게 되면 static변수, 정적변수, 또는 클래스 변수라 한다.

package static1;
public class DataCountMain3 {
 public static void main(String[] args) {
     Data3 data1 = new Data3("A");
     System.out.println("A count=" + Data3.count);
     Data3 data2 = new Data3("B");
     System.out.println("B count=" + Data3.count);
     Data3 data3 = new Data3("C");
     System.out.println("C count=" + Data3.count);
 }
}

실행결과
A count=1
B count=2
C count=3

count 정적 변수에 접근하는 방법은 Data3.count와 같이 .(dot)을 사용한다.

  • static이 붙은 멤버 변수는 메서드 영역에서 관리한다.
    • static이 붙은 멤버 변수 count는 인스턴스 영역에 생성되지 않는다. 대신에 메서드 영역에서 이 변수를 관리한다.
  •  생성자에는 count++이라는 코드가 있는데 이때 count는 정적변수여서 메서드 영역에 있는 count 값이 증가한다.

정리

static 변수는 쉽게 이야기해서 클래스인 붕어빵 틀이 특별히 관리하는 변수이다. 붕어빵 틀은 1개이므로 클래스 변 수도 하나만 존재한다. 반면에 인스턴스 변수는 붕어빵인 인스턴스의 수 만큼 존재한다.

 

static 변수 정리

public class Data3 {
 public String name;
 public static int count; //static
}

 

멤버 변수(필드)의 종류

  • 인스턴스 변수: static이 붙지 않은 멤버 변수 ex) name
    • static이 붙지 않는 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라서 인스턴스 변수라 한다.
    • 인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어진다.
  • 클래스 변수: static이 붙은 멤버 변수 ex) count
    • 클래스 변수, 정적 변수, static변수등으로 부른다. 용어를 모두 사용하니 주의!!
    • static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용 가능하고, 클래스 자체에 소속
    • 클래스 변수는 자바 프로그램을 시작할 때 딱 1개 만들어진다. 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용

변수와 생명주기

  • 지역 변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 스택 프레임에 포함된 지역변수도 함께 제거된다. 따라서 지역 변수는 생존 주기가 짧다.
  • 인스턴스 변수:인스턴스에 있는 멤버 변수를 인스턴스 변수라 한다. 인스턴스 변수는 힙 영역을 사용, 힙 영역은 GC가 발생하기 전까지는 생존하기 때문에 지역 변수보다 생존 주기가 길다.
  • 클래스 변수:클래스 변수는 메서드 영역의 static영역에 보관되는 변수이다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성된다. 그리고 JVM이 종료될 때 까지 생명주기가 이어진다. 따라서 가장 긴 생명주기를 가진다.

정적 변수 접근 법

//추가
//인스턴스를 통한 접근
Data3 data4 = new Data3("D");
System.out.println(data4.count);
//클래스를 통합 접근
System.out.println(Data3.count);

실행결과
4
4

 

인스턴스를 통한 접근 data4.count

정적 변수의 경우 인스턴스를 통한 접근은 추천 X, 왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것처럼 오해할 수 있기 때문

 

클래스를 통한 접근 Data3.count

정적변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확하다.

 

static 메서드

package static2;
public class DecoUtil2 {
 public static String deco(String str) {
 String result = "*" + str + "*";
 return result;
 }
}
package static2;
public class DecoMain2 {
 public static void main(String[] args) {
 String s = "hello java";
 String deco = DecoUtil2.deco(s);
 System.out.println("before: " + s);
 System.out.println("after: " + deco);
 }
}

실행 결 ```
before: hello java
after: *hello java*

만약 static이 안붙어 있었다면 DecoUtil1의 인스턴스를 먼저 생성해야 한다. 그런데 deco()라는 기능은 멤버 변수도 없고, 단순히 기능만 제공할 뿐이다. 인스턴스가 필요한 이유는 멤버 변수(인스턴스 변수)등을 사용하는 목적이 크다.

static 이 붙은 정적 메서드는 객체 생성 없이 클래스명 + . (dot) + 메서드 명으로 바로 호출할 수 있다.

 

static이 붙은 메서드를 정적 메서드 또는 클래스 메서드라 한다.

static이 붙지 않은 메서드는 인스턴스를 생성해야 호출할 수 있다. 이것을 인스턴스 메서드라 한다.

 

정적 메서드 사용법

  • static 메서드는 static만 사용할 수 있다.
    • 클래스 내부의 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있다.
    • 클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없다.
    • 인스턴스변수나 메서드는 인스턴스가 생성된후에 사용할수 있어서 안됨. 그리고  어떤걸 참조해야할지 모름.
  • 반대로 모든 곳에서 static을 호출할 수 있다.
    • 정적 메서드는 공용 기능이다. 따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static을 호출할 수 있다.
package static2;
public class DecoData {
 private int instanceValue;
 private static int staticValue;
 public static void staticCall() {
     //instanceValue++; //인스턴스 변수 접근, compile error
     //instanceMethod(); //인스턴스 메서드 접근, compile error
     staticValue++; //정적 변수 접근
     staticMethod(); //정적 메서드 접근
 }
 public void instanceCall() {
     instanceValue++; //인스턴스 변수 접근
     instanceMethod(); //인스턴스 메서드 접근
     staticValue++; //정적 변수 접근
     staticMethod(); //정적 메서드 접근
 }
 private void instanceMethod() {
 	System.out.println("instanceValue=" + instanceValue);
 }
 private static void staticMethod() {
 	System.out.println("staticValue=" + staticValue);
 }
}

 

해당 코드에서 staticCall()메서드 같은 경우 정적 메서드이기때문에 static만 사용가능
static 이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생한다.

 

instanceCall()

이 메서드는 인스턴스 메서드이기 때문에 모든 곳에서 공용인 static 을 호출할 수 있다. 따라서 정적 변수, 정적 메서드에 접근할 수 있다. 물론 인스턴스 변수, 인스턴스 메서드에도 접근할 수 있다.

 

정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유

정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스처럼 참조값의 개념이 없다.

특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출한다. 따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.

 

멤버 메서드의 종류

  •  인스턴스 메서드: static이 붙지 않은 멤버 메서드
  • 클래스 메서드: static이 붙은 멤버 메서드
    • 클래스 메서드, 정적메서드,static메서드등으로 불린다.

static이 붙지 않은 멤버 메서드는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다.

정적메서드는 객체 생성이 필요없이 메서드의 호출만으로 필요한 기능을 수행할 때 사용

 

반응형