static 변수 및 메소드는 언제 메모리에 할당되고 해제될까?


본 궁금증을 해결하기 위해 static 메모리 영역에 좀 더 관심을 갖고 공부해보자!

 

 

 

 

 

메모리 영역 살펴보기


 

Static 메모리에는 클래스, static 변수 및 메소드가 존재한다. Static 변수 및 메소드는 객체(인스턴스)를 생성하지 않고도 접근이 가능하며, 여러 객체(인스턴스)들이 공유한다. JVM이 프로그램을 시작할 때 할당되고, 프로그램을 종료할 때 할당 해제된다. GC에 의해 관리되지 않으며 생명주기가 프로그램의 시작부터 종료까지이므로 프로그램 실행 내내 메모리에 할당된 채 존재한다. 따라서 과용하게 되면 시스템의 성능에 악영향을 미칠 수 있다는 점을 생각해야 한다.

 

Heap 메모리에는 new 연산자에 의해서 생성된 객체(인스턴스)들이 존재하는 영역이다. GC에 의해 관리된다.

 

 

 

 

Static 변수 및 메소드의 특별함


일반적인(non-static) 클래스의 변수 및 메소드는 해당 클래스가 인스턴스화(new 연산자를 통한 생성)되기 전까지는 사용할 수 없다. 하지만 static 변수나 메소드는 인스턴스의 생성과 상관없이(즉, 객체를 생성하지 않고도) 접근하여 사용할 수 있다. 즉, 프로그램 시작 시 메모리에 고정적으로 할당되며 프로그램이 종료될 때 해제된다.

 

 

 

 

 

 

그래서, Static 변수와 메소드는


JVM의 실행과정에서 필요한 클래스의 정보를 메모리에 로딩한다. 이 로딩 시점에서 static 변수가 초기화된다.

따라서, 프로그램 시작 시 static 메모리에 할당되며 프로그램이 종료될 때 해제된다.

 

 

 

 

 


[출처]

 

 

kim-daeyong.github.io/2019-07-09-static/

 

static에 대해

오늘 면접에서 static의 생명주기에 대한 질문을 들었다. static의 용도나 메모리에 한번 올라가고~ 이런건 알았는데 static이 언제 생성되고 언제 소멸하는지는 대충은 생각하고 있었지만 정확한 때

kim-daeyong.github.io

 

mangkyu.tistory.com/m/47

 

[Java] static변수와 static 메소드

1. Static 정리 Java에서 Static 키워드를 사용한다는 것은 메모리에 한번 할당되어 프로그램이 종료될 때 해제되는 것을 의미합니다. 이를 정확히 이해하기 위해서는 메모리 영역에 대한 이해가 필

mangkyu.tistory.com

 

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하기


클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자(constructor)다.

 

이에 추가하여, 클래스는 정적 팩터리 메서드(static factory method)를 생성자와 별도로 제공할 수 있다.

정적 팩터리 메서드는 해당 클래스의 인스턴스를 반환하는 단순한 정적 메서드다.

 

따라서, 클래스는 클라이언트에 public 생성자와 정적 팩터리 메소드 둘 다 제공할 수 있고, 각각 장단점이 있다.

 

 

정적 팩터리 메소드의 장점

1. 이름을 가질 수 있으므로 반환될 객체의 특성을 묘사할 수 있다.

따라서, 한 클래스에 시그니처가 같은 생성자가 여러 개가 필요하다면, 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지으면 된다.

 

2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다. 따라서, 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하여 불필요한 객체 생성을 피할 수 있다. 생성 비용이 큰 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올릴 수 있다. 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있는데 이러한 클래스를 인스턴스 통제 클래스라고 한다.

 

3. 반환 타입의 하위 타입 객체를 반환할 수 있다. 즉, 반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성을 가질 수 있다. API를 만들 때 이를 응용하면 구현 클래스를 공개하지 않고 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다. 이는 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크의 핵심 기술이기도 하다.

 

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

 

5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다. 이런 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다.

 

 

정적 팩터리 메소드의 단점

1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다. 이는 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들 때 제약을 지켜야 한다는 점에서 오히려 장점이 될 수도 있다.

 

2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다. 생성자처럼 API 설명에 드러나지 않기 때문에 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다. 또한, API 문서를 잘 써놓고 메서드 이름을 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야 한다.

 

 

 

 

아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라


정적 팩터리와 생성자 모두는 선택적 매개변수가 많을 때 적절히 대응하기가 어렵다.

 

이렇게, 매개변수 개수가 많아지면 첫 번째 대안으로 점층적 생성자 패턴도 가능하긴 하지만, 클라이언트 코드를 작성하거나 읽기 어렵다. 클라이언트가 실수로 매개변수의 순서를 바꾸어도 컴파일러는 알아채지 못하고, 런타임에 엉뚱한 동작을 하게 된다.

 

선택 매개변수가 많을 때 활용할 수 있는 두 번째 대안으로 자바빈즈 패턴이 있다. 매개변수가 없는 생성자로 객체를 만들고, 세터 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다. 이 또한 심각한 단점을 가지고 있다. 자바빈즈 패턴에서는 객체 하나를 만들려면 메서드를 여러개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다. 일관성이 깨진 객체가 만들어지면, 버그를 심은 코드와 그 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 떨어져 있어 디버깅이 어렵다. 일관성이 무너지기 때문에 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없다.

 

세 번째 대안이 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴이다. 클라이언트는 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다. 그 다음 빌더 객체가 제공하는 일종의 세터 메서들들로 원하는 선택 매개변수들을 설정한다. 마지막으로 매개변수가 없는 build메서드를 호출해 우리에게 필요한 객체를 얻는다. 

 

따라서, 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그런데, 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

 

 

아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라


싱글턴(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 싱글턴의 대표적인 예로 함수와 같은 무상태 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있다.

 

싱글턴을 만드는 방식은 보통 둘 중 하나다. 두 방식 모두 생성자는 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해둔다.

 

세 가지 방법을 소개한다.

첫 번째는 public static 멤버가 final 필드인 방식, 두 번째는 정적 팩터리 메서드를 public static 멤버로 제공하는 방식, 세 번째는 원소가 하나인 열거 타입을 선언하는 것이다.

 

대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다. 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다. (열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다.)

 

 

아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라


정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때 (ex. java.lang.Math, java.utio.Arrays),

특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드를 모아놓을 때 (ex. java.util.Collections),

final 클래스와 관련한 메서드들을 모아놓을 때, 즉 final 클래스를 상속해서 하위 클래스에 메서드를 넣는 것을 막을 때,

 

private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다. 추가로, 상속을 불가능하게 하는 효과도 있다.

 

컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이니, 명시적 생성자가 private이라면 클래스 바깥에서는 접근할 수 없게 된다. private 생성자에 'throw new AssertionError();' 코드를 추가한다면 Assertion Error를 던져 클래스 안에서 실수로라도 생성자를 호출하지 않도록 할 수 있다.

 

 

 

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라


많은 클래스가 하나 이상의 자원에 의존한다.

 

이 때, 정적 유틸리티 클래스로 구현하거나, 싱글턴을 잘못 사용한다면, 유연하지 않고 테스트하기 어렵다.

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.

 

대신, 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 하는 조건을 만족하는 패턴이 존재한다.

이는 의존 객체 주입의 한 형태로, 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.

 

의존 객체 주입은 자원이 몇 개든 의존 관계가 어떻든 상관없이 잘 작동하며, 불변을 보장하여 여러 클라이언트가 의존 객체들을 안시하고 공유할 수 있게 한다. 또한, 생성자, 정적 팩터리, 빌더 모두에 똑같이 응용할 수 있다.

 

이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체이다. 즉, 팩터리 메서드 패턴을 구현한 것인데, 대표적으로 Supplier<T> 인터페이스가 팩터리를 표현한 예시이다.

 

의존 객체 주입이 유연성과 테스트 용이성을 개선해주긴 하지만, 의존성이 수천 개가 되는 큰 프로젝트에서는 코드를 어지럽게 만들기도 한다. 이는 스프링(Spring) 같은 의존 객체 주입 프레임워크를 사용하면 이러한 어질러짐을 해소할 수 있다.

 

 

 

아이템 6. 불필요한 객체 생성을 피하라


똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.

 

생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.

생성 비용이 비싼 객체가 반복해서 필요할 때는 캐싱하여 재사용하길 권장한다. 성능 개선을 위해서 불변인 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에 이 인스턴스를 재사용한다.

 

하지만, 불필요한 객체를 만들어내는 오토박싱을 피하자. 오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술인데, 성능 부분에서 큰 저하가 발생할 수 있다.

 

 

 

아이템 7. 다 쓴 객체 참조를 해제하라


프로그램에서 특정 객체들을 더이상 사용하지 않더라도 스택이 그 객체들의 다 쓴 참조를 여전히 가지고 있다면 가비지 컬렉터가 회수하지 않는다. 따라서 메모리 누수가 발생한다. 가비지 컬렉션 언어에서는 의도치 않게 객체를 살려두어 발생하는 메모리 누수를 찾기가 까다롭다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 객체가 참조하는 모든 객체와 그 객체들이 참조하는 모든 객체까지도 회수해가지 못한다. 그래서 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다.

 

특히, 자기 메모리를 직접 관리하는 클래스(ex. stack)라면 프로그래머는 항시 메모리 누수에 주의해야 한다. 원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null 처리해줘야 한다.

 

캐시, 리스너 혹은 콜백 또한 메모리 누수의 주범인데, 이는 WeakHashMap을 사용하여 캐시를 만들거나, 콜백을 약한 참조로 저장하면 가비지 컬렉트가 적절히 수거해갈 수 있다.

 

 

 

아이템 8. finalizer와 cleaner 사용을 피하라


자바에서 제공하는 두 객체 소멸자 finalizer와 cleaner는 사용을 지양하자.

파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 finalizer나 cleaner를 사용하는 대신에, AutoCloseable을 구현하고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다.

 

 

 

 

아이템 9. try-finally 보다는 try-with-resources를 사용하라


자바 라이브러리에는 InputStream, OutputStream, java.sql.Connection 등과 같이 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 하므로, 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 쓰인다.

 

하지만, try-finally 문을 사용한 방식에는 결점이 존재한다. 따라서, 자바 7에서 도입된 try-with-resources를 사용하자.

이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다. 따라서, 닫아야하는 자원을 뜻하는 클래스를 작성한다면 AutoCloseable을 반드시 구현하도록 하자. try-with-resources 버전이 코드가 짧고 읽기 수월하며 만들어지는 예외 정보도 유용하여 문제를 진단하기도 좋고 정확하고 쉽게 자원을 회수할 수 있다.

 

그러므로, 꼭 회수해야 하는 자원을 다룰 때는 모든 케이스에 대해서 try-finally 말고, try-with-resources를 사용하자.

'Java' 카테고리의 다른 글

[Java] static 메모리의 생명주기  (0) 2021.05.03
[Java] 정적 변수와 정적 메소드 (static)  (0) 2021.03.30

static


static은 "정적인" , "고정적인" 이라는 뜻을 가지고 있다.

변수와 메소드의 이름 앞에 static을 붙이면 그것은 정적 변수 또는 정적 메소드가 된다.

 

이렇게 만들어진 정적 변수와 정적 메소드는 객체의 인스턴스에 소속되는 것이 아닌, 클래스에 고정되었다고 할 수 있다.

따라서, 클래스 로더가 클래스를 로딩하여 메모리 영역에 적재할때 클래스별로 관리된다. 그러므로 클래스의 로딩이 끝난 후부터 바로 사용이 가능하다.

 

static 멤버(static 변수 + static 메소드)는 static 메모리에서 별도로 관리되며, 이 메모리는 모든 객체가 공유한다.

이는 객체들이 주로 할당하며 GC(Garbage Collector)가 관리해주는 Heap 메모리와는 다르게, 클래스들이 할당하며, GC(Garbage Collector)가 관리하지 않는다는 특징이 있다.

 

그렇기 때문에, static을 불필요하게 많이 사용할 경우 해당 프로그램이 종료될 때 까지 static 메모리가 그대로 유지되므로 시스템의 성능에 악영향을 줄 수 있으므로  이 부분에 대해서 주의해야 한다. 

 

 

 

 

메모리 측면에서의 이점


static을 붙이게 되면 해당 변수 또는 메소드에 대한 메모리 할당을 한 번만 하게되며, 이는 메모리 사용에 있어서 이점을 가져온다.

 

 

공유 측면에서의 이점


static을 붙이게 되면 해당 변수 또는 메소드는 하나의 주소에 위치하게 되며, 이를 전체에서 공유하게 된다.

+ Recent posts