객체 참조 해제란
메서드 내에서 사용할 객체를 인스턴스화 하여 로컬 변수에 할당하여 참조하고 사용 후 더 이상 사용하지 않는다고 명시적 null처리로 참조 관계를 끊어주거나 해당 메서드가 종료되면 더 이상 사용하지 않을 인스턴스로 참조 관계를 해지하는 것.
보통은 메서드 종료시 변수가 유효 범위를 벗어나게 되어 더 이상 사용하지 않아 가비지 컬렉터가 참조 관계가 끊어진 객체를 회수하는게 가장 좋지만 그렇지 않은 예외적인 경우도 있다.
책에서 나온 예제 코드를 본다면
package effectiveJava.item7;
import java.util.Arrays;
import java.util.EmptyStackException;
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 클래스 생성하면 elements 배열에 16개의 방이 초기화
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
// elements에 인자값 넣기 후치 증감 연산자로 0부터 차례로 들어간다.
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
// elements 배열에 참조된 값 가져오기 전치 감소 연산자로 늘어난 size값을 -1하여 마지막 배열 값부터 가져온다.
// 단순 값을 가져오는 것일 뿐 배열의 방을 빼는건 아니다.
public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
// 기본 16개 배열의 방이 꽉 차면 방을 2배로 늘려준다.
public void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
// elements 배열 방의 갯수를 알려준다.
public int elementsLength() {
return elements.length;
}
public static void main(String[] args) {
Stack stack = new Stack();
System.out.println("Stack size : " + stack.size);
System.out.println("Stack elements length : " + stack.elementsLength());
System.out.println("Stack elements[0] : " + stack.elements[0]);
System.out.println("-------------------------------");
stack.push("hello1");
stack.push("hello2");
stack.push("hello3");
stack.push("hello4");
stack.push("hello5");
stack.push("hello6");
stack.push("hello7");
stack.push("hello8");
stack.push("hello9");
stack.push("hello10");
stack.push("hello11");
stack.push("hello12");
stack.push("hello13");
stack.push("hello14");
stack.push("hello15");
stack.push("hello16");
stack.push("hello17");
System.out.println("Stack size : " + stack.size);
System.out.println("Stack elements length : " + stack.elementsLength());
System.out.println("Stack elements[size] : " + stack.elements[stack.size]);
System.out.println("-------------------------------");
System.out.println(stack.pop());
System.out.println("-------------------------------");
System.out.println("Stack size : " + stack.size);
System.out.println("Stack elements length : " + stack.elementsLength());
System.out.println("Stack elements[size] : " + stack.elements[stack.size]);
}// end of main
}// end of class
결과값을 보면
Stack 인스턴스 생성하며 필드 size 값 0 초기화
elements 배열 크기 16개 초기화
elements 배열 0번째 값 없음
그 후 push 메서드로 배열의 방을 16개 까지 채우고 연산한 뒤에 후치 증감으로 size는 17이 되고
elements 배열은 방이 16개가 되면서 ensureCapacity() 메서드가 실행 -> 2배 +1 만큼 방이 늘어나 33개의 방이 되고
elements 방에 16번째 방까지 채워져 있으니 17번째에는 아무것도 없어서 null
pop() 메서드 실행시 size 값이 1 줄면서 elements 방의 16번째 값인 hello17 출력
size 는 16, elements 방의 갯수는 그대로 33개, pop() 메서드는 해당 배열의 값을 출력 해줄 뿐 해당 방의 값을 없에거나 실질적으로 뺴는 행위를 하지 않으니 elements[16]번 값의 출력은 그대로 hello17이 나온다.
이렇듯 추가하는대로 값을 추가하고 pop()이라는 메서드를 실행한다고 해도 실질적인 배열에서 빼주지 않는다면 가비지 컬렉터가 회수 하지 않아 오래 사용할 시 메모리 누수가 나고 가비지 컬렉션 활동과 메모리 사용량이 늘어나 심할 때는 디스크 페이징이나 OutOffMemoryError를 일으켜 프로그램이 종료될 수 있다.
해법은 해당 참조를 다 썼을 때 null 처리(참조 해제) 하면 된다.
package effectiveJava.item7;
import java.util.Arrays;
import java.util.EmptyStackException;
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 클래스 생성하면 elements 배열에 16개의 방이 초기화
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
// elements에 인자값 넣기 후치 증감 연산자로 0부터 차례로 들어간다.
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
// elements 배열에 참조된 값 가져오기 전치 감소 연산자로 늘어난 size값을 -1하여 마지막 배열 값부터 가져온다.
// 단순 값을 가져오는 것일 뿐 배열의 방을 빼는건 아니다.
/*public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}*/
// elements 배열에 참조된 값 가져오기 전치 감소 연산자로 늘어난 size값을 -1하여 마지막 배열 값부터 가져온다.
// 값을 가져오는 동시에 해당 배열의 방을 참조 해제해준다.
public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null; // 참조 해제
return result;
}
// 기본 16개 배열의 방이 꽉 차면 방을 2배로 늘려준다.
public void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
// elements 배열 방의 갯수를 알려준다.
public int elementsLength() {
return elements.length;
}
public static void main(String[] args) {
Stack stack = new Stack();
System.out.println("Stack size : " + stack.size);
System.out.println("Stack elements length : " + stack.elementsLength());
System.out.println("Stack elements[0] : " + stack.elements[0]);
System.out.println("-------------------------------");
stack.push("hello1");
stack.push("hello2");
stack.push("hello3");
stack.push("hello4");
stack.push("hello5");
stack.push("hello6");
stack.push("hello7");
stack.push("hello8");
stack.push("hello9");
stack.push("hello10");
stack.push("hello11");
stack.push("hello12");
stack.push("hello13");
stack.push("hello14");
stack.push("hello15");
stack.push("hello16");
stack.push("hello17");
System.out.println("Stack size : " + stack.size);
System.out.println("Stack elements length : " + stack.elementsLength());
System.out.println("Stack elements[size] : " + stack.elements[stack.size]);
System.out.println("-------------------------------");
System.out.println(stack.pop());
System.out.println("-------------------------------");
System.out.println("Stack size : " + stack.size);
System.out.println("Stack elements length : " + stack.elementsLength());
System.out.println("Stack elements[size] : " + stack.elements[stack.size]);
}// end of main
}// end of class
이렇게 사용후 참조 해제가 되야하는 경우(앞으로 다시 쓸일이 없는) 해당 값을 다시 사용할 때 nullPointException이 나서 오류를 발견할 수 있다.
객체 참조를 null 처리를 수동으로 해야하는 경우는 예외적인데 Stack 클래스가 자기 메모리를 직접 관리하기 때문이다.
객체 자체가 아닌 객체 참조를 담는 배열로 저장소 Pool을 만들어 원소값들을 관리한다.
가비지 컬렉터는 배열이 모두 빈값으로 참조 값에 모두 null이 들어가 있다면 더이상 사용하지 않을 것임을 알겠지만 Stack클래스에서는 pop()메서드의 행동에서 참조 null 처리를 안할 시 그대로 배열에 값이 들어가 있는 것이니
가비지 컬렉터가 여전히 사용중인 객체로 인식하는 것.
쓰지 않을 참조값이라면 null 처리를 해서 해당 객체를 쓰지 않을 것임을 가비지 컬렉터에게 알려줘야 한다.
일반적으로 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항상 메모리 누수에 주의해야 한다.
배열, List나 Map, Set 같은 컬렉션 같은 경우 어딘가에 객체를 쌓아두고 메모리를 직접 다루는 객체들은 메모리누수를 염두해 둬야 한다.
캐시 또한 메모리 누수를 염두해둬야 하는데
List나 Map 으로 만든 캐시 Pool 공간에 정보를 가진 인스턴스들의 참조값을 쌓아두기만 하고 제거하는 행위가 없을 때
캐쉬는 계속 쌓여만 간다.
이를 해결하는 방법은 여러가지 중 캐시 키를 참조하는 동안 엔트리가 살아있는 캐쉬가 필요한 상황이라면 HashMap 대신 WeakHashMap을 사용할 수 있다. 다 쓴 엔트리는 가비지 컬렉션이 일어날 때 자동으로 같이 제거된다.
package effectiveJava.item7.cache;
import java.util.HashMap;
import java.util.Map;
public class PostRepository {
private Map<CacheKey, Post> cache;
public PostRepository() {
this.cache = new HashMap<>();
}
public Post getPostById(Integer id) {
CacheKey key = new CacheKey(id);
if (cache.containsKey(key)) {
return cache.get(key);
} else {
// TODO DB에서 읽어오거나 REST API를 통해 읽어올 수 있습니다.
Post post = new Post();
cache.put(key, post);
return post;
}
}
public Map<CacheKey, Post> getCache() {
return cache;
}
}
클라이언트가 id 값에 해당하는 글을 가져올 때 캐시에 담겨져 있다면 캐시에서 글을 가져오고 없다면 새로 만들어서 캐시에 심은뒤 해당 글을 던져주는 객체
여기에서도 캐시를 삭제하는 별도의 행위는 없다. 캐시가 쌓이면 쌓이는데로 계속 추가만 된다.
package effectiveJava.item7.cache;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class PostTest {
@Test
public void cache() throws InterruptedException {
PostRepository postRepository = new PostRepository();
Integer p1 = 1;
postRepository.getPostById(p1);
assertFalse(postRepository.getCache().isEmpty());
System.out.println("run gc");
// 가비지 컬렉션이 일어난다는 보장은 없지만 어쨌든 실행 됨
System.gc();
System.out.println("wait");
Thread.sleep(3000L);
System.out.println("캐쉬남아있나? : " + postRepository.getCache());
// 에러
assertTrue(postRepository.getCache().isEmpty());
}
}
id값에 해당하는 글을 요청하는 클라이언트.
Post 인스턴스를 가져온 후 가비지 컬렉션이 일어난다.
결과는 어떻게 될까?
run gc가 찍히고 가비지 컬렉션이 일어났지만 여전히 캐쉬는 남아있어서 true가 아니기 때문에 테스트 실패가 난다.
이럴 때 HashMap을 WeakHashMap으로 변경하여 getPostById메서드가 종료되면 key값이 로컬변수라 참조 해지가 되면서 WeakHashMap에 해당 키값, 바인딩된 value 값이 가비지 컬렉션이 실행되면서 제거가 된다.
package effectiveJava.item7.cache;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
public class PostRepository {
private Map<CacheKey, Post> cache;
public PostRepository() {
// HashMap -> WeakHashMap
this.cache = new WeakHashMap<>();
}
public Post getPostById(Integer id) {
// id값을 Integer를 쓴 이유
// 가져온 id값을 해당 메서드에서 CacheKey 객체 key값으로 세팅했기 때문에
// 이 메서드가 끝나면 key 값이 참조 해제가 된다.
// 만약 클라이언트에서 CacheKey 객체에 key 값을 할당했다면 메서드가 끝나기 전
// 가비지 컬렉션이 이루어졌기 때문에 캐시가 남아있게 된다.
CacheKey key = new CacheKey(id);
if (cache.containsKey(key)) {
return cache.get(key);
} else {
// TODO DB에서 읽어오거나 REST API를 통해 읽어올 수 있습니다.
Post post = new Post();
cache.put(key, post);
return post;
}
}
public Map<CacheKey, Post> getCache() {
return cache;
}
}
getPostById 메서드가 끝나면서 key값이 유효범위를 넘어가며 더이상 사용하지 않는 참조 변수가 되었고
WeakHashMap에 사용한 key값이 더이상 유효하지 않는 참조변수 일 때 가비지 컬렉션이 일어나면 해당 key,value 값이 제거되는 것
다른 캐시삭제 방법으로는 유효기간을 설정하여 기간이 지나면 제거하는 방식을 사용한다.
(LRU(Least Recently Used) 캐시 : 최근에 자주 사용한 캐시들만 남기고 지우는 것)
백그라운드 스레드를 돌리는 방식이 있다.
주기적으로 가장 오래된 캐시를 찾아서 제거하는 알고리즘을 적용한다.
ScheduledThreadPoolExecutor를 활용하여 백그라운드 스레드로 스케쥴링을 돌려 일정 주기동안은 가장 오래된 캐시들을 삭제하거나 새로운 객체가 생성되면 부수 작업으로 기존의 객체를 지우는 방식으로 처리할 수 있다.
@Test
public void backgroundThread() throws InterruptedException {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
PostRepository postRepository = new PostRepository();
// AutoBoxing
postRepository.getPostById(1);
postRepository.getPostById(2);
postRepository.getPostById(3);
postRepository.getPostById(4);
postRepository.getPostById(5);
postRepository.getPostById(6);
postRepository.getPostById(7);
postRepository.getPostById(8);
// 캐시에서 가장 오래된 캐시부터 삭제하는 스레드
Runnable removeOldCache = () -> {
System.out.println("running removeOldCache task");
Map<CacheKey, Post> cache = postRepository.getCache();
Set<CacheKey> cacheKeys = cache.keySet();
Optional<CacheKey> key = cacheKeys.stream().min(Comparator.comparing(CacheKey::getCreated));
key.ifPresent((k) -> {
System.out.println("removing " + k);
cache.remove(k);
});
};
System.out.println("The time is : " + new Date());
// 1초 후부터 3초 간격으로 removeOldCache 스케쥴링 실행
executor.scheduleAtFixedRate(removeOldCache, 1, 3, TimeUnit.SECONDS);
Thread.sleep(20000L);
executor.shutdown();
}
WeakHashMap을 다시 HashMap으로 변경하고 진행해야 한다.
마지막으로 리스너 혹은 콜백에서 나는 메모리 누수일 경우
콜백 : 이벤트가 발생하면 특정 메서드 호출
리스너 : 이벤트가 발생하면 연결된 리스너에게 이벤트 전달
채팅방 어플리케이션을 생각했을 때
채팅방에 유저가 입장하면 유저 객체가 리스트에 담긴다.
채팅방에서 채팅 메세지를 남기면 모든 유저에게 해당 메세지가 전달되는 리스너가 있다고 할 때
유저가 나가기 버튼을 눌러서 이벤트가 발생하면 해당 유저 참조 해제를 하면 되지만
그게 아닌 경우 그냥 나가졌을 때는??
여전히 메세지 전달하는 이벤트에 나간 유저 참조가 해제되지 않아 계속 쌓이게 될 수 있다.
이 것 역시 메세지 전달하는 이벤트가 일어났을 때 가비지 컬렉션이 일어나면 떠나간 유저가 있다면 WeakReference로 약한 참조가 해제 되며 유저리스트에서 제거가 되게 만든다.
WeakHashMap
List나 Map에 들어간 엔트리들은 가비지컬렉션이 일어나도 안에 들어간 엔트리들이 바로 제거되지 않는다.
WeakHashMap에 들어간 객체들은 더 이상 사용하지 않을때 가비지컬렉션이 일어나면 자동으로 제거된다.
StrongReference = 보통 new로 생성하여 할당하는 것들 List list = new ArrayList<>(); 메서드 종료시, 해당 인스턴스 참조하는 변수가 없으면 가비지 컬렉션 대상이 됨
SoftReference = 어떤 인스턴스를 StrongReference로 할당된게 있고 해당 인스턴스를 SoftReference로 할당된 변수가 있다고 가정할 때 StrongReference 참조 관계가 해지가 되고 SoftReference만 참조하고 있을 경우 가비지 컬렉션이 일어났을 때 메모리에 공간이 필요할 때만 제거 대상이 된다. 메모리가 충분하다면 제거되지 않는다.
WeakReference = 어떤 인스턴스를 StrongReference로 할당된게 있고 해당 인스턴스를 WeakReference로 할당된 변수가 있다고 가정할 때 StrongReference 참조 관계가 해지되고 WeakReference만 참조하고 있을 경우 가비지 컬렉션이 일어나면 무조건 제거 대상이된다.
PhantomReference = 왜쓰냐? 자원 정리할 때 언제 객체가 사라졌는지 알 수 있다.
PhantomReference만 참조관계로 남았을 때 가비지 컬렉션이 일어나면 메모리에서 제거가 되고 레퍼런스 큐로 이동이 된다. 사용후 따라 레퍼런스 큐에서 제거해주면 된다.
ThreadPool
package effectiveJava.item7.executor;
public class ExecutorsExample {
public static void main(String[] args) {
Thread thread = new Thread(new Task());
thread.start();
System.out.println(Thread.currentThread() + " hello");
}// end of main
static class Task implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + " world");
}
}// end of class
}// end of class
서브 스레드 생성 방법
메인스레드와 별도로 서브스레드 생성 Runnable을 구현한 클래스를 만들고
서브스레드를 시작한다.
메인스레드에서 hello를 찍고 서브 스레드가 2초 뒤에 world를 찍음
executor를 사용 하는 이유 스레드가 여러개 필요할 때 스레드가 시스템 리소스를 많이 잡아먹음
만약 100개의 스레드를 돌려야 했을 때 executor를 사용하여 적은 스레드 양으로 동일한 양의 작업을 처리할 수 있다면?
스레드 풀을 만들어서 사용
package effectiveJava.item7.executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsExample {
public static void main(String[] args) {
// 스레드풀에 스레드를10개 만들어 100개의 작업을 돌림
ExecutorService service = Executors.newFixedThreadPool(10);
// 필요한 만큼 스레드를 돌림 무한정 스레드를 생성할 수 있으니 조심
ExecutorService service2 = Executors.newCachedThreadPool();
// 한 개의 스레드로 작업 처리
ExecutorService service3 = Executors.newSingleThreadExecutor();
// 작업을 딜레이시켜서 실행하거나 주기적으로 실행시킬 때
ExecutorService service4 = Executors.newScheduledThreadPool(10);
for (int i = 0; i < 100; i++) {
service.submit(new Task());
}
System.out.println(Thread.currentThread() + " hello");
service.shutdown();
}// end of main
static class Task implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + " world");
}
}// end of class
}// end of class
스레드를 돌릴 때 해당 로직이 CPU를 많이 쓰는지 IO를 많이 쓰는지에 따라 스레드 갯수를 잘 지정해 줘야 한다.
연산을 많이 하는 작업일 경우 CPU 갯수에 따라 스레드 갯수를 생성하여 처리하는 것이 좋다.
int numberOfCpu = Runtime.getRuntime().availableProcessors(); <- CPU 갯수 구하기
IO 위주의 작업이라면 네트워크를 사용하거나 DB에서 값을 가져올 때 시간이 얼마나 걸리는지 CPU 갯수와 상관없이 각 작업의 적절한 스레드 갯수를 알아내서 사용
'Effective Java > 정리' 카테고리의 다른 글
[Item 6] 불필요한 객체 생성을 피하라 (0) | 2023.01.17 |
---|---|
Item8. finalizer와 cleaner 사용을 피하라 (0) | 2023.01.16 |
정적 유틸리티 클래스 (Static Utility Class) (0) | 2023.01.09 |
[Item 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.01.09 |
[Item 4] 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2023.01.09 |