[스택과 힙 영역]
data클래스 파일
package memory;
public class Data {
private int value;
public Data(int value) { // 생성자 alt+insert
this.value = value;
}
public int getValue(){
return value;
}
}
javamemoryMain
package memory;
public class JavaMemoryMain2 {
public static void main(String[] args) {
System.out.println("main start");
method1(); // alt+enter누르면 함수 만들어줌
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()-> method1()-> method2() 순서로 호출하는 단순한 코드이다.
method1()에서 Data클래스의 인스턴스를 생성한다.
method1()에서 method2()를 호출할 때 매개변수에 Data인스턴스의 참조값을 전달한다.
<실행 과정>
1. 처음 main() 메서드를 실행한다. main()스택 프레임이 생성된다.
2. main()에서 method1()을 실행한다. method1() 스택 프레임이 생성된다.
3. method1()은 지역 변수로 Data data1을 가지고 있다. 이 지역 변수도 스택 프레임에 포함된다.
4. method1()은 new data(10)을 사용해서 힙 영역에 Data인스턴스를 생성한다. 그리고 참조값을 data1에 보관한다.
5. method1()은 method2()를 호출하면서 Data data2 매개변수에 x001 참조값을 넘긴다.
6. 이제 method1()에 있는 data1과 method2()에 있는 data2지역 변수(매개변수 포함)는 둘 다 같은 x001 인스턴스를 참조한다.
7. method2()가 종료되면, method2()의 스택 프레임이 제거되면서 매개변수 data2도 함께 제거된다.
8. method1()이 종료된다. method1()의 스택 프레임이 제거되면서 지역면수 data1도 함께 제거된다.
9. method1()이 종료된 직후의 상태를 보면, method1()의 스택 프레임이 제거되고, 지역변수 data1도 함께 제거되었다.
10. 이제 x001 참조값을 가진 Data인스턴스를 참조하는 곳은 더는 없다.
11. 참조하는 곳이 없으므로 사용되는 곳도 없다. 결과적으로 프로그램에서 더는 사용하지 않는 객체인 것이다. 이런 객체는 메모리만 차지하게 된다.
12. GC(가비지 컬렉션)은 이렇게 참조가 모두 사라진 인스턴스를 찾아서 메모리에 제거한다.
자바는 사용하지 않는 객체를 gc이 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거한다.
참고: 힙 영억 외부가아닌, 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우에도 GC의 대상이 된다.
정리: 지역 변수는 스택 영역에, 객체 인스턴스는 힙 영역에 관리되는 것을 확인했다. 이제는 메서드 영역을 확인해본다. 메서드 영역이 관리하는 변수도 있다. 이것을 이해하기 위해서는 먼저 static키워드를 알아야 한다. static키워드는 메서드 영역과 밀접한 연관이 있다.
[STATIC]
static키워드는 주로 멤버 변수와 메서드에 사용된다.
특정 클래스를 통해 생성된 객체 수를 세는 단순한 프로그램
인스턴스 내부 변수에 카운트 저장
생성된 객체 수를 세어야 한다. 따라서 객체가 생성될 때마다 생성자를 통해 인스턴스의 멤버 변수인 count값을 증가시킨다.
public class Data1 {
public String name;
public int count;
public Data1(String name){
this.name=name;
count++;
}
}
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("A count=" + data2.count);
Data1 data3= new Data1("C");
System.out.println("A count=" + data3.count);
}
}
이렇게 했는데 답이 1,1,1이 나왔다.
어떻게 수정해야 할까? -> 생성할떄마다 변수를 서로 공유한다.
package Static1;
public class Counter {
public int count; // 공유해서 객체가 생성될때마다 이 값을 증가시킨다.
}
package Static1;
public class Data2 {
public String name;
public Data2(String name, Counter counter){ // 클래스에서 가져옴
this.name=name;
counter.count++;
}
}
package Static1;
public class DataCountMain2 {
public static void main(String[] args) {
Counter counter = new Counter();
Data2 data1=new Data2("A", counter);
System.out.println("A count= " + counter.count);
Data2 data2=new Data2("B", counter);
System.out.println("B count= " + counter.count);
}
}
counter인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때마다 값을 정확하게 증가시킬 수 있다.
그런데 여기 불편한 점들이 있다.
1. counter라는 별도의 클래스를 추가해야 한다.
2. 생성자의 매개변수도 추가되고, 생성자가 복잡해진다. 생성자를 호출하는 부분도 복잡해진다.
클래스 추가 없이 공용 변수를 만들 수 있는 방법은 없을까?
특정 클래스에 공용으로 함께 사용할 수 있는 변수를 만들 수 있다면 편리할 것이다.
static키워드를 사용하면 공용으로 함께 사용하는 변수를 만들 수 있다.
public class Data3 {
public String name;
public static int count;
public Data3(String name){
this.name=name;
count++;
}
}
1. 기존 코드를 유지하기 위해 새로운 클래스 Data3을 만들었다.
2. static int count를 보면,int 앞에 static이 있다
3. 이렇게 멤버 변수에 static으 붙이게 되면, static변수(정적/ 클래스 변수)리고 한다. -> 앞에 외부 클래스를 만든 것과 동일한 효과를 보인다!
4. 객체가 생성되면 생성자에서 정적 변수 count의 값을 하나 증가시킨다.
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("A count=" + Data3.count);
Data3 data3= new Data3("C");
System.out.println("A count=" + Data3.count);
}
}
static이 붙은 멤버 변수는 메서드 영역에서 관리한다.(붕어빵 틀에서 관리한다.)
1. static이 붙은 멤버 변수 count는 인스턴스 영역에 생성되지 않는다. 대신에 메서드 영역에서 이 변수를 관리한다.
2. Data3("A") 인스턴스를 생성하면 생성자가 호출된다.
3. 생성자는 count++코드가 있다. count는 static이 붙은 정적 변수이다. 정적 변수는 인스턴스 영역이 아니라 메서드 영역에서 관리한다. 따라서 이 경우 메서드 영역에 있는 count의 값이 하나 증가된다.
4. Data3("B")인스턴스를 생성하면 생성자가 호출된다.
5. count++코드가 있다. count는 static이 붙은 정적 변수이다. 메서드 영역에 있는 count변수의 값이 하나 증가된다.
6. Data3("C") 인스턴스를 생성하면 생성자가 호출된다.
7. count++코드가 있다. count는 static이 붙은 정적 변수이다. 메서드 영역에 있는 count변수의 값이 하나 증가된다.
8. 최종적으로 메서드 영역에 있는 count변수의 값은 3이 된다.
9. static이 붙은 정적 변수에접근하려면 Data3.count와 같이 클래스명.변수명 으로 접근하면 된다.
10. 참고로 Data3의 생성자와 같이 자신의 클래스에 있는 정적 변수라면 클래스명을 생략할 수 있다.
11. static변수를 사용한 덕분에 공용 변수를 사용해서 편리하게 문제를 해결할 수 있었다.
정리: static변수는 쉽게 이야기해서 클래스인 붕어빵 틀이 특별히 관리하는 변수이다. 붕어빵 틀은 1개 이므로 클래스 변수도 하나만 존재한다. 반면에 인스턴스인 붕어빵은 인스턴스의 수 만큼 변수가 존재한다.
멤버 변수(필드)의 종류
인스턴스 변수 | static이 붙지 않은 변수 ex) name 1. static이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있따. 따라서 인스턴스 변수라 한다. 2. 인스턴스 변수는 인스턴스를 만들 때마다 새로 만들어진다. |
클래스 변수 | static이 붙은 변수 ex) count 1. 클래스/ 정적/ static 변수 등으로 부른다. 용어를 모두 사용 2. static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있다. 따라서 클래스 변수라 한다. 3. 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어진다. 인스턴스와는 다르게 보통 여러 곳에서 공유하는 목적으로 사용된다. |
변수와 생명주기
지역변수 (매개변수 포함) |
지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프레임도 제거되는데, 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거된다. 따라서 지역 변수는 생존주기가 짧다. |
인스턴스 변수 | 인스턴스에 있는 멤버 변수를 인스턴스 변수. 힙영역을 사용한다. 힙 영역은 GC가 발생하기 전까지 생존하기 때문에 보통 지역변수보다 생존 주기가 길다. |
클래스 변수 | 메서드 영역의 static영역에 보관디는 변수이다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간. 클래스 변수는 해당 클래스가 JVM에 로딩되는 순간 생성된다. 그리고 JVM이 종료될 때까지 생명주기가 이어진다. 따라서 긴 생명주기를 가진다. |
static이 정적이라는 이유는 바로 여기에 있다. 힙 영역에 생성되는 인스턴스 변수는 동적으로 생성되고, 제거된다. 반면에 static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거된다. 정적 변수는 이름 그대로 정적이다.
정적 변수 접근법
static변수는 클래스를 통해 바로 접근할 수도 있고, 인스턴스를 통해서 접근할 수 있다.
DataCountMain3마지막 코드에 다음 부분을 추가하고 실행해보자
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("A count=" + Data3.count);
Data3 data3= new Data3("C");
System.out.println("A count=" + Data3.count);
//추가(인스턴스를 통한 접근)
Data3 data4=new Data3("D");
System.out.println(data4.count);
// 클래스를 통한 접근
System.out.println(Data3.count);
// -> 개념상 static영역의 count로 간다.
}
}
인스턴스 접근 권장하지 않는 이유: 정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않는다. 왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것 처럼 오해할 수 있기 떄문
클래스를 통한 접근: 정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확하다. 따라서 정적 변수에 접근할 때는 클래스를 통해서 접근하자.
[Static메서드]
인스턴스와 메서드
package static2;
public class DecoUtil1 {
public String deco(String str){
return "*" + str + "*";
}
}
package static2;
public class DecoMain1 {
public static void main(String[] args) {
String s="hello java";
DecoUtil1 utils=new DecoUtil1();
String deco=utils.deco(s);
System.out.println("before: " + s);
System.out.println("after: " + deco);
}
}
DecoUtil2는 앞선 예제와 비슷한데, 메서드 앞에 static이 붙어 있다. 이렇게 하면 정적 메서드를 만들 수 있다. 그리고 정적 메서드는 정적 변수처럼 인스턴스 생성 없이 클래스명을 통해서 바로 호출
* static* 이 붙으면 모두 클래스 소속
package static2;
public class DecoUtil1 {
public static String deco(String str){ // static이 있어야 인스턴스 생성없이 클래스 만으로 호출 가능
return "*" + str + "*";
}
}
package static2;
public class DecoMain1 {
public static void main(String[] args) {
String s="hello java";
String deco=DecoUtil1.deco(s); // 이렇게 수정할 수 있다.
System.out.println("before: " + s);
System.out.println("after: " + deco);
}
}
정적 메서드 덕분에 불필요한 객체 생성 없이 편리하게 메서드를 사용했다.
클래스 메서드: 메서드 앞에도 static을 붙일 수 있다. 이것을 정적 메서드 또는 클래스 메서드라 한다. 정적 메서드 라는 용어는 static이 정적이라는 뜻이기 떄문이고, 클래서 메서드라는 용어는 인스턴스 생성 없이 마치 클래스에 있는 메서드를 바로 호출하는 것 처럼 느껴지기 때문이다.
그렇다면? static해서 인스턴스 안만들고 사용하면 되지 굳이 인스턴스를 사용하는 이유는????
-> 정적 메서드는 언제나 사용할 수 있는 것은 아니다.
정적 메서드 사용 방법
1. static메서드는 static만 사용할 수 있다.
- 클래스 내부 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있다.
- 클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.
2. 반대로 모든 곳에서 static을 호출할 수 있다.
- 정적 메서드는 공용 기능이다. 따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static을 호출할 수 있다.
staticCall()메서드를 보면 정적 메서드이다. 따라서 static만 사용할 수 있다. 정적 변수, 정적 메서드에는 접근할 수 있지만, static이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생한다.
코드를 보면 staticCall()-> staticMethod()로 static에서 static을 호출하는 것을 확인할 수 있다.
instanceCall() 메서드를 보자.
이 메서드는 인스턴스 메서드이다. 모든 곳에서 공용인 static을 호출할 수 있다. 따라서 정적변수, 정적 메서드에 접근할 수 있다. 물론 인스턴스 변수, 인스턴스 메서드에도 접근할수 있다.
-> 인스턴스 변수에 접근하려면 참조 값이 있어야 한다.
정적 메서드가 인스턴스 기능을 사용할 수 없는 이유
정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스처럼 참조값의 개념이 없다.
특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출한다. 따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.
정적 메서드 -> 클래스명
인스턴스 -> 참조값
* 단, 외부에서 참조값이 넘어오는 경우에는 static이든 아니든 아무런 관계가 없다 *
정적 메서드 활용
정적 메서드는 객체 생성이 필요 없이 메서드의 호출만으로 필요한 기능을 수행할 때 주로 사용한다.
예를 들어, 간단한 메서드 하나로 끝나는 유틸리티성 메서드에 자주 사용한다. 수학의 여러가지 기능을 담은 클래스를 만들 수 있는데, 이 경우 인스턴스 변수 없이 입력한 값을 계산하고 반환하는 것이 대부분이다. 이럴 때 정적 메서드를 사용하여 유틸리티성 메서드를 만들면 좋다
정적 메서드의 접근 -> 인스턴스 접근(비추) vs 클래스를 통한 접근
특정 클래스의 정적 메서드 하나만 적용하려면 다음과 같이 생략한 메서드 명을 적어준다.
import static static2.DecoData.StaticCall;
특정 클래스의 모든 정적 메서드를 적용하려면 다음과 같이 *을 사용하면 된다.
import static static2.DecoData.*;
참고로 import static은 정적 메서드 뿐만 아니라 정적 변수에도 사용할 수 있다.
[Main메서드]
인스턴스 생성 없이 실행하는 가장 대표적인 메서드
프로그램 시작점이 되는데, 생각해보면 객체를 생성하지 않아도 main메서드가 작동했다. 이것은 main 메서드의 static이기 때문이다.
정적 메서드는 정적 메서드만 호출할 수 있다. 따라서 정적 메서드인 main이 호출되는 메서드에는 정적 메서드를 사용한다. 더 정확히 말하자면 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있다. 따라서 정적 메서드인 main()메서드가 같은 클래스에서호출하는 메서드도 정적 메서드로 선언해서 사용했다.
[Final변수와 상수]
변수에 final키워드가 붙으면 더는 값을 변경할 수 없음.
final은 class, method를 포함한 여러 곳에서 붙을 수 있다.
파라미터에도 final을 넣을 수 있다.
이미 넘겨주는 값에서 고정되어서 함수 안에서 값을 바꾸는 것은 불가능하다.
1. final을 지역변수에 설정할 경우 최초 한 번만 할당할 수 있다. 이후에 변수의 값을 변경하려면 컴파일 오류가 발생한다.
2. final을 지역 변수 선언 시 바로 초기화 한 경우 이미 값이 할당 되었기 때문에 값을 할당할 수 없다
3. 매개변수에 final이 붙으면 메서드 내부에서 매개변수의 값을 변경할 수 없다. 따라서 메서드 호출 시점에 사용된 값이 끝까지 사용된다.
package final1;
public class ConstrucInit {
final int value; //final이 값은 생성자를 통해서 넣어줘야 한다.
public ConstrucInit(int value){
this.value=value; // 이후에는 이 값을 바꾸는 것은 불가능하다.
}
이처럼 final을 필드에서 사용할 경우에는 생성자를 통해서 한 번 초기화를 할 수 있다.
package final1;
public class FieldInit {
static final int CONST_VAULE=10; // 이미 초기값 넣음
final int value=10;
public FieldInit(int value) {
this.value=value; // 이미 값이 할당이 되어서 생성자에서 바꿀 수 없다.
}
}
즉, final은 한 번 세팅되면 끝
package final1;
public class ConstrucInit {
final int value; //final이 값은 생성자를 통해서 넣어줘야 한다.
public ConstrucInit(int value){
this.value=value; // 이후에는 이 값을 바꾸는 것은 불가능하다.
}}
public class FinalFieldMain {
public static void main(String[] args) {
// final 필드 - 생성자 초기화
System.out.println("생성자 초기화");
ConstrucInit construcInit1=new ConstrucInit(10);
ConstrucInit construcInit2=new ConstrucInit(20);
System.out.println(construcInit1.value);
System.out.println(construcInit2.value);
System.out.println("필드 초기화");
FieldInit fieldInit1=new FieldInit();
FieldInit fieldInit2=new FieldInit();
FieldInit fieldInit3=new FieldInit();
System.out.println(fieldInit1.value);
System.out.println(fieldInit2.value);
System.out.println(fieldInit3.value);
// 상수
System.out.println("상수");
System.out.println(FieldInit.CONST_VAULE);
// 자바에서는 "static final"이 붙은 것을 상수라고 한다.
}
}
ConstructInit과 같이 생성자를 사용해서 final필드를 초기화하는 경우, 각 인스턴스마다 final필드에 다른 값을 할당할 수 있다. 물론 final을 사용했기 때문에 생성 이후에 이 값을 변경하는 것은 불가능하다.
* 생성자 초기화와 다르게 필드 초기화는 필드의 코드에 해당 값이 정해져 있기 때문이다. 모든 인스턴스가 같은 값을 사용하기 때문에 결과적으로 메모리를 낭비하게 된다.(물론 JVM에 따라서 내부 최적화시도를 할 수 있다.) 또 메모리 낭비를 떠나서 같은 값이 계속 생성되는 것은 개발자가 보기에는 명백한 중복이다. 이럴 때 사용하기 좋은 것은 static영역이다!
[static final]
1. 공용 변수인데, 바뀌지 않는 공용 변수가 된다. final키워드를 사용해서 초기화 값이 변하지 않는다.
2. static영역은 단 하나만 존재하는 영역이다. MY_VALUE변수는 JVM상에서 하나만 존재하므로 앞서 설명한 중복과 메모리 비효율 문제를 모두 해결할 수 있다.
이런 이유로 필드에 final + 필드 초기화를 사용하는 경우 static을 붙여서 사용하는 것이 효과적이다.
상수란? 변하지 않고, 항상 일정한 값을 갖는 수를 말한다. 자바에서는 보통 "단 하나만 존재하는 변하지 않는 고정된 값"을 상수라 한다. static final키워드를 사용한다.
자바 상수 특징
1. static final키워드를 사용한다.
2. 대문자를 사용하고 구분은 _(언더바)로 한다(관례)
- 일반적인 변수와 상수를 구분하기 위해 이렇게 한다.
3. 필드를 직접 접근해서 사용한다.
- 상수는 기능이 아니라 고정된 값 자체를 사용하는 것이 목적이다.
- 상수는 값을 변경할 수 없다. 따라서 필드에 직접 접근해도 데이터가 변하는 문제는 발생하지 않는다.
package final1;
public class Constant {
//수항 상수
public static final double PI =3.14;
//시간 상수
public static final int HOURS_IN_DAY=24;
public static final int MINUTES_IN_DAY=24;
public static final int SECOND_IN_DAY=24;
// 애플리케이션 설정 상수
public static final int MAX_USERS=24;
}
1. 보통 상수들은 APP전반에서 사용되기 때문에 public을 자주 사용한다. 물론 특정 위치에서만 사용된다면 다른 접근 제어자를 사용하면 된다.
2. 상수는 중앙에서 값을 하나로 관리할 수 있다는 장점도 있다.
3. 상수는 런타임에 변경할 수 없다.(실행 도중에 변경x) 상수를 변경하려면 프로그램을 종료하고, 코드를 변경한 다음에 프로그램을 다시 실행해야 한다.
+ 추가로 상수는 중앙에서 값을 하나로 관리할 수 있다는 장점도 있다.
package final1;
public class ConstantMain1 {
public static void main(String[] args) {
System.out.println("프로그램 최대 참여자 수 " + 1000);
int currentUserCount=999;
process(currentUserCount++); //999
process(currentUserCount++); //1000
process(currentUserCount++); //1001
process(currentUserCount++); //1002
}
private static void process(int currentUserCount) {
System.out.println("참여자 수: "+currentUserCount);
if(currentUserCount>1000){
System.out.println("대기자로 등록합니다.");
}else{
System.out.println("게임에 참여합니다.");
}
}
}
만약 최대 참여자 수를 1000이 아닌 2000을 고쳐야한다면?? 고쳐할 부분이 많아짐.
-> 이럴떄 상수 사용해서 한번에 변경가능
package final1;
public class ConstantMain1 {
public static void main(String[] args) {
System.out.println("프로그램 최대 참여자 수 " + Constant.MAX_USERS); // 이렇게하면 한 번만 변경하면 됨
int currentUserCount=999;
process(currentUserCount++); //999
process(currentUserCount++); //1000
process(currentUserCount++); //1001
process(currentUserCount++); //1002
}
private static void process(int currentUserCount) {
System.out.println("참여자 수: "+currentUserCount);
if(currentUserCount>Constant.MAX_USERS){
System.out.println("대기자로 등록합니다.");
}else{
System.out.println("게임에 참여합니다.");
}
}
}
package final1;
public class Constant {
//수항 상수
public static final double PI =3.14;
//시간 상수
public static final int HOURS_IN_DAY=24;
public static final int MINUTES_IN_DAY=24;
public static final int SECOND_IN_DAY=24;
// 애플리케이션 설정 상수
public static final int MAX_USERS=24; // 여기만 변경하면 됨
}
[final 변수와 참조]
final을 참조에 사용하면? 주소값을 변경할 수 없다.
package final1;
public class Data {
public int value;
}
package final1;
public class FinalReMain {
public static void main(String[] args) {
final Data data = new Data(); //참조형
// 참조가 된 값은 변경할 수 없다. 이 이후로 초기화 한 번 더 불가능
//참조 대상의 값은 변경이 가능하다.
data.value=10;
System.out.println(data.value);
data.value=20;
System.out.println(data.value);
}
}
주소값 대입은 끝났는데, 참조 대상 안에 멤버 변수는 변경이 가능하다! (그 안에 값은 변경할 수 있다)