static
static은 '정적인', '고정적인'이라는 사전적 의미를 가진다.
Java에서 static 키워드를 사용한다는 것은 메모리에 한번 할당되어 프로그램이 종료될 때 해제되는 것을 의미한다.
static을 이해하기 위해서는 먼저 JVM을 이해할 필요가 있다.
JVM (Java Virtual Machine)
- java 애플리케이션을 실행하면 JVM은 OS로부터 메모리를 할당한다.
- 자바 컴파일러(javac)가 자바 소스코드(Xxx.java)를 바이트 코드(Xxx.class)로 컴파일한다.
- Class Loader를 통해 JVM으로 로딩한다.
- 로드된 클래스 파일(Xxx.class)들은 기계가 읽을 수 없으므로 Execution Engine을 통해 기계어로 변환하여 실행한다.
- 이 과정에서 Execution Engine에 의해 GC 등도 작동된다.
Runtime Data Areas
JVM 메모리 영역인 Runtime Data Areas는 Method Area, Heap Area, Stack Area, PC Register, Native Method Stacks 총 5가지로 구분된다.
이 중에서 static을 이해하는 데 필요한 영역은 Method Area(Static Area), Heap Area, Stack Area로 3가지이다.
- Method Area (Static Area)
- JVM이 클래스 파일(Xxx.class)을 읽어 클래스 데이터를 저장하는 영역이다.
- 초기 로드가 필요한 정보들인 패키지 클래스, 인터페이스, 상수, static 변수, final 변수, 클래스 멤버 변수 등이 로드된 후 메모리에 항상 상주하고 있는 영역이다.
- Method Area(Static Area)에 할당된 메모리는 모든 객체가 공유하는 메모리라는 장점이 있다.
- GC의 관리 영역 밖에 있기 때문에 static을 자주 사용하면 프로그램의 종료시점까지 메모리가 할당된 채로 존재하므로 시스템 성능에 좋지 않다.
- Heap Area
- 메서드 안에서 사용되는 객체들을 위한 영역이다.
- Method Area(Static Area)에 로드된 클래스로만 객체 생성이 가능하다.
- new를 통해 생성된 객체(인스턴스)의 데이터와 참조 자료형(class, interface, enum, Array 등)이 저장된다.
- Heap Area의 메모리는 GC를 통해 수시로 관리받는다.
- 인스턴스가 생성될 때 힙에 생성되며 GC가 메모리를 수거할 때 소멸된다.
- Stack Area
- 메서드의 작업에 필요한 메모리 공간을 제공하는 영역이다.
- 메서드가 호출되면 개별적으로 Stack Area에 메모리가 할당되고, 해당 메서드가 수행되는 동안 필요한 값 등을 저장하는 데 사용된다.
- 메서드에서 직접 사용할 지역 변수, 파라미터, 리턴 값, 참조 변수 등이 주소 값으로 저장된다.
- 메서드 실행이 완료되면 할당되었던 메모리 공간은 반환된다.
static과 메모리 구조
클래스 로더가 클래스 파일(Xxx.class)을 탐색 중 static 키워드를 보는 순간 객체가 생성되지 않아도 항상 메모리를 할당해야 하는 멤버 변수로 보고 Method Area(Static Area)에 메모리를 할당한다.
그래서 static 키워드가 붙은 멤버 변수들은 객체(인스턴스)에 소속된 변수가 아니라 클래스에 소속된 변수이기 때문에 클래스 변수라고 부른다.
new를 통해 객체를 생성하면 각 인스턴스는 서로 독립적이지만 static 키워드가 붙은 멤버 변수들은 모든 객체가 메모리 영역을 공유한다.
따라서 아래와 코드처럼 객체 인스턴스는 다르지만 하나의 멤버 변수를 바라보게 된다.
public class CounterExample {
public static int count = 0;
CounterExample() {
count++;
System.out.println(count);
}
}
public class StaticCounterExample {
public static void main(String[] args) {
CounterExample counterExample1 = new CounterExample();
CounterExample counterExample2 = new CounterExample();
}
}
/* 출력 결과
1
2
*/
static 메서드 안에서 사용할 변수들이 메모리에 올라가는 순서 때문에 아래 코드는 불가능하다.
public class CounterExample {
public int count = 0;
// public static int count = 0;
public static int getCount() {
return count;
}
}
static이 붙은 멤버 변수나 메서드가 메모리에 먼저 올라가서 `getCount()` 메서드 안에 있는 count에서 오류가 발생한다.
이를 해결하기 위해서는 `count` 변수를 static으로 선언하면 된다.
static 이슈
- static 변수는 글로벌 변수에 가까우므로 인스턴스 변수보다 테스트가 까다로워진다.
- static 변수는 객체지향 프로그램의 원칙인 각 객체의 데이터들이 캡슐화되어야 한다는 원칙에 어긋난다.
- static 변수를 공유한 순간 서로에게 영향을 주어 어떤 사이드 효과가 발생할지 모른다.
- 오버라이딩을 할 수 없으므로 코드의 재사용성이 떨어진다.
- 프로그램이 종료되기 전까지 메모리에 존재해서 자주 사용하지 않는 메서드가 누적된다면 GC에서 반환되지 않으므로 메모리 낭비가 발생한다.
위와 같은 이유들이 있어서 단순히 호출 시간이 짧아진다고 static 변수 및 메서드 사용은 지양해야 한다.
여러 개의 인스턴스 생성을 피하고 싶다면 스레드 안정성을 갖는 싱글톤 패턴을 이용하여 클래스를 설계하는 것이 좋다.
static 좋은 사례
static은 자주 쓰는 객체나 만드는데 오래 걸리고 메모리를 많이 사용하는 객체에 사용하면 좋다.
public class SpellCheckerExample {
public boolean hasRomanNumeral(String inputText) {
return inputText.matches("^(?=[MDCLXVI])M*D?C{0,4}L?X{0,4}V?I{0,4}$");
}
}
위 코드는 로마자를 확인하는 메서드인데 `matches()` 메서드의 내부 로직을 보면 무거운 `Pattern` 객체로 컴파일하는 `new Pattern(regex, 0)`이 있다.
따라서 `matchs()` 메서드를 호출할 때마다 `Pattern` 객체 생성 부분을 `static final`로 따로 선언해서 사용하면 효율적이다.
public class SpellCheckerExample {
private static final Pattern ROMAN = Pattern.compile( "^(?=[MDCLXVI])M*D?C{0,4}L?X{0,4}V?I{0,4}$" );
public boolean hasRomanNumeral(String inputText) {
return ROMAN.matcher(inputText).matches();
}
}
비용이 많이 드는 `Pattern` 부분을 `static final`을 통해 한 번만 초기화해서 사용하는 로직으로 변경하면 메모리 낭비가 줄어들고 속도가 향상된다.
static 변수
- 클래스 변수
- 한 클래스에서 공통적인 값을 유지해야 할 때 선언
- 클래스가 메모리에 로딩될 때 생성되어 프로그램이 종료될 때까지 유지
- 메모리에 한번 할당되면 여러 객체가 해당 메모리를 공유 가능
- 객체를 생성하지 않고도 '클래스명.변수명'으로 호출 가능
- 예제 1
public class Fruit {
private String fruitName = "apple";
public void printFruitName() {
System.out.println(this.fruitName);
}
}
100개의 Fruit 객체를 생성하면 "apple"이라는 같은 값을 갖는 메모리가 100개가 중복해서 생성된다.
이런 경우 `static`을 사용하면 여러 객체가 하나의 메모리를 참조하도록 하여 메모리 효율이 더 높아질 것이다.
일반적으로 `static`은 상수일 경우가 많으므로 절대 변하지 않고 수정이 불가능하도록 `final`을 붙여준다.
public class Fruit {
public static final String FRUIT_NAME = "apple";
public void printFruitName() {
System.out.println(this.fruitName);
}
}
- 예제 2
// static 변수 (클래스 변수)
public class StaticExample {
public static final String DESCRIPTION = "static 변수";
}
// 인스턴스 변수
public class NonStaticExample {
public String description = "인스턴스 변수";
}
// 출력 테스트
public class StaticDescriptionExample {
public void printDescription() {
// static 변수는 인스턴스를 생성하지 않아도 사용할 수 있다.
System.out.println("static 변수 = " + StaticExample.ESCRIPTION);
// 인스턴스 변수는 인스턴스를 생성해야 사용할 수 있다.
NonStaticExample nonStaticExample = new NonStaticExample();
System.out.println("인스턴스 변수 = " + nonStaticExample.dexription);
}
}
/* 출력 결과
static 변수 = static 변수
인스턴스 변수 = 인스턴스 변수
*/
static 메서드
- 클래스 메서드
- 인스턴스 변수를 사용할 수 없으므로 인스턴스와 관계없는 메서드를 클래스 메서드(static 메서드)로 정의
- 객체를 생성하지 않고도 '클래스명.메서드명'으로 호출 가능
// static 변수, static 메서드 (클래스 변수, 클래스 메서드)
public class StaticExample {
// 매개변수가 필요하다.
public static long add(long a, long b) {
return a + b;
}
}
// 인스턴스 변수, 인스턴스 메서드
public class NonStaticExample {
long a;
long b;
// 인스턴스 변수 a, b만 사용하므로 매개변수가 필요 없다.
public long add() {
return a + b;
}
}
public class StaticAddExample {
public void printAdd() {
// static 메서드는 매개변수가 필요하다.
long num1 = 1.1;
long num2 = 2.2;
System.out.println("static add() = " + StaticExample.add(num1, num2));
// non-static 메서드는 매개변수가 필요없고, 인스턴스 메서드 생성 후 인스턴스 변수에 대입해줘야 한다.
NonStaticExample nonStaticExample = new NonStaticExample();
nonStaticExample.a = 1.1;
nonStaticExample.b = 2.2;
System.out.println("non-static add() = " + nonStaticExample.add());
}
}
/* 출력결과
static add() = 3.3
non-static add() = 3.3
*/
References
https://mangkyu.tistory.com/47
https://honbabzone.com/java/java-static/
https://brunch.co.kr/@artiveloper/14