정적(Static)이란?
정적(static)은 고정된이란 의미를 가지고 있다. Static이라는 키워드를 사용하여 Static변수와 Static메소드를 만들 수 있는데 다른말로 정적필드와 정적 메소드라고도 하며 이 둘을 합쳐 정적 멤버라고 한다. (클래스 멤버라고도 한다.)
정적 필드와 정적 메소드는 객체(인스턴스)에 소속된 멤버가 아니라 클래스에 고정된 멤버다. 그렇기에 클래스 로더가 클래스를 로딩해서 메소드 메모리 영역에 적재할 때 클래스 별로 관리된다. 따라서 클래스의 로딩이 끝나는 즉시 바로 사용할 수 있다.
방금 static에 대한 대략적인 부분을 설명했지만 좀 더 자세한 내용을 덧붙여서 다시 한번 더 설명하고자 한다.
Static Keyword, Method Area
static은 사전적으로 '정적인, 고정된, 움직이지 않는'의 의미를 갖는다. jvm 메모리에 고정됨을 뜻함과 동시에 static을 사용한다는 의미는 모든 객체가 '공유'한다는 의미를 뜻한다.
인스턴스 변수의 값이 인스턴스 마다 다른 값을 가질 수 있다는 점은 하나의 클래스를 여러개의 인스턴스로 만들어서 사용할 수 있다는 점에서 좋은 기능이라고 할 수 있다.
그러나, 때에 따라서 모든 인스턴스가 같은 값을 공유하게 하고 싶을 때가 있다. 이런 경우 해당 변수를 클래스의 멤버로 만들면 된다. 인스턴스 변수와 마찬가지로 class 내부에 위치 하지만, static 키워드를 멤버 앞에 붙이면 클래스의 멤버가 된다. 즉, 해당 클래스 소속의 변수가 된다. 물론, 메소드도 마찬가지이다.
static이 실행되는 시점은 클래스가 메모리상에 올라갈 때이다. 즉, 우리가 프로그램을 실행하면 필요한 클래스가 jvm 메모리상에 로딩되는 과정을 거친다. 그리고 한번 로딩된 클래스는 특별한 일이 발생하지 않는 이상 메모리상에서 객체를 생성할 수 있도록 메모리에 상주한다. static은 이 시점에 메모리에 올라가면서 필요한 동작을 처리한다. 결론적으로 static은 객체의 생성과는 관계없이 클래스가 로딩되는 시점에 단 한번만 필요한 동작을 처리하기 위해 사용한다. 이 때, jvm 의 메소드 영역(클래스 영역)에 클래스의 정보들이 올라가게 된다.
이해를 돕기위한 JVM의 메모리 구조의 메소드 영역(클래스 영역)에 대한 간략한 설명이다.
프로그램이 실행되면 JVM은 OS로부터 메모리를 할당 받고, 그 메모리를 용도에 따라 여러 영역으로 나누어 관리한다. 이번 포스팅의 핵심인 메소드 영역에 대해서 자세히 보면, 프로그램을 수행하기 위해 OS에서 할당받은 메모리 공간인 Runtime Data Area 안에 포함되어 있다.
메소드 영역은 Class Area, Code Area, Static Area로 불려지며, 의미상 공유 메모리 영역이라고도 불린다. 코드에서 사용되는 클래스들을 클래스 로더로 읽어 클래스 별로 런타임 상수 풀(runtime constant pool), 필드(field) 데이터, 메소드(method) 데이터, 메소드 코드, 생성자 코드 등을 분류해서 저장한다. 위에서도 말했지만, 메소드 영역은 JVM이 동작해서 클래스가 로딩될때 생성되고, 모든 스레드가 공유하는 영역이다.
static이 붙은 변수를 클래스 변수라고 하는 것은 위에 그림에서 확인할 수 있듯이 변수가 존재하는 영역이 클래스가 존재하는 영역과 같기 때문이다.
정적(Static) 멤버 생성
Static 키워드를 통해 생성된 정적멤버들은 Heap영역이 아닌 Static(Class, Code)영역에 할당된다. Static 영역에 할당된 메모리는 모든 객체가 공유하여 하나의 멤버를 어디서든지 참조할 수 있는 장점을 가지지만 Garbage Collector의 관리 영역 밖에 존재하기에 Static영역에 있는 멤버들은 프로그램의 종료시까지 메모리가 할당된 채로 존재하게 된다. 참고로, 정적 객체는 GC에 의해 수집될 수 있다.
동적은 객체를 런타임 도중에 힙 영역에 할당하는 행위를 말한다. 반대로, 정적은 프로그램이 시작되는 시점에 클래스 로더가 클래스를 해석하여 메소드 영역 혹은 힙 영역에 클래스 메타 데이터 및 정적 변수를 적재한다.
그렇기에 Static을 너무 남발하게 되면 만들고자 하는 시스템 성능에 악영향을 줄 수 있다.
위에서 계속 static은 Class Area, Code Area, Static Area에 저장된다고 얘기하다가 갑자기 힙 영역이 등장했다...
그 이유에 대해 살펴보자
Heap? Method Area?? 그래서 Static이 저장되는 위치는???
static이 저장되는 위치를 알기 전에 permanent 영역과 metaspace 영역을 이해해야 한다.
Permanent vs Metaspace
- Permanent
- Java 8 이전까지 존재했던 메소드 영역
- 클래스 내부의 메타 데이터를 저장하는 영역
- Heap 영역에 속하며 클래스, 메소드 메타 데이터, 정적 객체, 정적 변수, 상수 풀 등을 관리했다.
- Java 8 이후부터는 Metaspace 영역으로 대체되었다.
- 특이하게 Heap 영역 안에 존재했던 메소드 영역임을 기억하자.
- Metaspace
- Java 8부터 생긴 영역으로 permanent 영역이 관리하던 일부 정보를 저장한다.
- 메소드 데이터 영역에 속하는 동시에 네이티브 메모리 영역(JVM이 아닌 OS에서 관리)에 속한다.
Java 8 이전
Java 8 이전의 힙을 보면, Permanent 영역이 존재하고 이 안에 클래스 메타 데이터, 정적 변수 등이 저장된다. 이때, Permanent 영역은 메소드 영역에 해당하므로 Java 8 이전 static 변수는 메소드 영역에 저장되는 것이 맞다.
Java 8 이후
Java 8 이후의 힙을 보면, Permanent 영역이 사라졌다. 대신 힙 외부에서 Metaspace라고 하는 네이티브 메모리에서 Permanent가 저장하던 정보를 들고 있도록 바뀌었다. 이때 주의할 점은 permanent가 저장하던 정보를 전부 Metaspace에서 관리하는 것은 아니다.
JDK 8의 레퍼런스를 살펴 보자면, Permanent 영역에서 관리하던 클래스 메타 데이터는 Metaspace로 옮겨지고 interned String과 클래스 정적 변수는 힙 영역에 옮겨진다는 사실을 알 수 있다. 즉, java 8 이후부터 static을 힙 영역에서 관리하게 된다.
왜 Permanent 영역이 사라졌을까?
이유는 단순하다. 각종 클래스 메타 데이터를 힙 영역에서 관리하다 보니, 힙 영역에 들어간 데이터의 사이즈를 계산하기 어려워서 OOM이 자주 발생하곤 했다. 그래서 클래스 메타 데이터는 네이티브 메모리에서 관리하고, 정적 변수만 힙 영역에서 관리하게 된 것이다.
자바는 클래스와 인터페이스의 메타 데이터를 java.lang 패키지에 소속된 Class 클래스로 관리한다.
메타 데이터란? 클래스의 이름, 생성자 정보, 필드 정보, 메소드 정보를 말한다.
Garbage Collection
Java 8 이전이든 이후든, 정적 객체는 GC 수집에 대상이 된다. 그래서 프로그램 전체 동안 정적 변수가 사라지지 않는다는 것은 일부만 맞는 말이고, 참조를 잃은 정적 객체는 언제나 GC 수집에 대상이 될 수 있다. 당연한 이야기지만 primitive 타입의 정적 변수는 프로그램 종료 전까지 살아남게 된다.
Static을 지양해야 하는 이유
- 메모리 문제
- static은 프로그램 실행 시점에 메모리에 할당을 하며, 웬만하면 프로그램 종료 시점까지 메모리에서 해제되지 않는다.
- 동시성 이슈 문제
- static은 전역에서 접근이 가능하므로 별도의 동기화 전략을 수립해야 한다.
- 런타임 다형성 불가
- static으로만 이루어진 메소드를 사용하는 객체의 경우, 해당 객체를 메모리로 할당하여 사용하는 것이 아니고 객체.메소드로 바로 접근하여 호출한다.
- 객체의 상태를 이용할 수 없다.
- 정적 메소드를 사용하기 위해서는 필요로 하는 인자를 모두 외부에서 주입해야 한다. 정적 메소드 안에는 클래스의 인스턴스 필드를 사용할 수 없기 때문이다.
- static은 프로그램 시점에 메모리에 올라가는데, 정적 메소드 안에 객체의 인스턴스 필드가 초기화되지 않았다면 문제가 생길 수 있다. 그래서 정적 메소드 안에는 정적 변수만 사용할 수 있다.
- 일반 메소드라면 객체 내의 있는 상태를 통해 해당 메소드를 구현해 줄 수 있으므로 변화하는 상태에 따라 다채로운 기능 구현이 가능하다.
- 결국 객체 내의 정적 메소드가 많아지면, 외부 값에 의존하는 수동적인 객체가 되어 버린다.
- 정적 메소드를 사용하기 위해서는 필요로 하는 인자를 모두 외부에서 주입해야 한다. 정적 메소드 안에는 클래스의 인스턴스 필드를 사용할 수 없기 때문이다.
- 테스트하기 어려움
- 정적 필드는 전역으로 관리되기 때문에 프로그램 전체에서 이 필드에 접근하고 수정할 수 있다. 따라서 해당 필드를 추론하기 어려워 테스트하기 까다롭다.
Static은 언제 사용할까?
위 단점만 보면 static은 절대 쓰면 안될 것처럼 보인다. 개인적인 생각으로는 static은 되도록 지양하는 것이 좋으나, 사용하면 좋은 상황들이 몇몇 있다.
상수 정의
절대 변하지 않는 변수를 상수라고 하는데, 해당 상수는 객체 내에서 매번 일반 변수로 정의하기 보다는 한 번 정적 변수로 정의하면 메모리를 아낄 수 있다.
private static final String NAME = "닐슨";
유틸리티 클래스 정의
유틸리티 클래스는 인스턴스 메소드와 인스턴스 변수를 제공하지 않고, 데이터 처리를 위한 정적 메소드만 존재하는 클래스를 말한다. java에서 Math 클래스를 보면, 상수 외에 인스턴스 변수가 하나도 없고 계산을 위한 정적 메소드만 제공한다.
이렇게 애초부터 객체의 상태를 이용할 생각이 없고, 여러 객체들의 필요에 의해 데이터를 처리하는 공통 로직이 필요할 때는 static을 사용하여 설계한다.
참고