본문 바로가기
Java/Java

메모리 정리하는 방법(null, Weak/SoftReference, Background Thread, 메모리 직접 관리)

by oneny 2023. 8. 6.
메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있다. 이런 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 한다. 그래서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요하다.

 

메모리 누수가 발생할 수 있는 대표적인 상황 3가지

  • Stack -> Array
  • Cache -> Map
  • Listener -> List

위 3가지 예제 모두 공통적으로 필드로 객체를 쌓아두는 공간이 있다. 즉, 자기 메모리를 직접 관리하기 때문에 메모리 누수에 취약한 것이다. 객체를 쌓아두는 공간에 활성 영역에 속한 요소들은 사용되고 비활성 영역은 쓰이지 않는데 문제는 가비지 컬렉터가 이 사실을 알 길이 없다. 가비지 컬렉터가 보기에는 비활성 영역에서 참조하는 객체도 똑같이 유효한 객체다. 따라서 비활성 영역의 객체가 더 이상 쓸모없다는 건 프로그래머만 아는 사실이기 때문에 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에 알려야 한다.

 

메모리를 정리하는 방법

위 메모리 누수가 발생할 수 있는 상황을 위해 메모리를 정리하는 방법에는 크게 4가지가 있다.

  1. 직접 null을 처리하는 것
  2. 특정한 자료구조(WeakReference)를 사용하는 것(ex. 캐시가 필요한 상황에서 WeakHashMap 사용)
  3. 캐시에 새 엔트리를 추가할 때 부수 작업을 수행하여 직접 메모리 관리하는 것
  4. Background Thead를 사용해서 주기적으로 CleanUp하는 작업을 실행하는 것(ScheduledThreadPoolExcutor)

 

메모리를 정리하는 첫 번째 방법(Stack)

public class Stack {

  private Object[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;

  public Stack() {
    elements = new Object[DEFAULT_INITIAL_CAPACITY];
  }

  public void push(Object e) {
    ensureCapacity();
    elements[size++] = e;
  }

  public Object pop() {
    if (size == 0) {
      throw new EmptyStackException();
    }

    return elements[--size];
  }

  /**
   * 원소를 위한 공간을 적어도 하나 이상 확보한다.
   * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
   */
  private void ensureCapacity() {
    if (elements.length == size) {
      elements = Arrays.copyOf(elements, 2 * size + 1);
    }
  }

  public static void main(String[] args) {
    Stack stack = new Stack();
    for (String arg : args) {
      stack.push(arg);
    }

    while (true) {
      System.err.println(stack.pop());
    }
  }
}

위 코드에서 메모리 누수가 발생하는 부분은 어디일까? pop() 메서드를 보면 elements 배열에 elements[--size];로 접근해서 반환한다. 그러면 계속해서 elements의 요소들이 사라지지는 않고 쌓이기만 하기 때문에 메모리 누수가 생겨 언젠가는 OOM(OutOfMemory)가 발생할 수 있다.

 

public Object pop() {
  if (size == 0) {
    throw new EmptyStackException();
  }

  Object result = elements[--size];
  elements[size] = null; // 다 쓴 객체 해체
  return result;
}

따라서 pop() 메서드를 통해 elements의 요소를 빼낼 때, elements[size] = null;을 통해 해당 인덱스에 있는 요소를 null로 해제해서 해당 객체가 GC의 대상이 되도록 만들어줌으로써 메모리 누수를 방지할 수 있다. 

이처럼 Stack처럼 요소를 넣고 뺴는 작업에서 가비지 컬렉션의 대상이 되지 않는 예외적인 경우가 발생할 수 있기 때문에 메모리를 직접 관리한다면 메모리 누수에 주의해야 한다. 즉, 이러한 쌓인 객체들이 언제 사라져야 하는가를 염두하면서 코드를 작성해야 한다.

 

메모리 정리하는 두 번째 방법 (WeakHashMap)

public class Post {

  private Integer id;
  private String title;
  private String content;

  // ... getter, setter

  @Override
  public void finalize() {
    System.out.println("gc called");
  }
}
public class CacheKey {

  private Integer value;

  private LocalDateTime created;

  public CacheKey(Integer value) {
    this.value = value;
    this.created = LocalDateTime.now();
  }

  @Override
  public boolean equals(Object o) {
    return this.value.equals(o);
  }

  @Override
  public int hashCode() {
    return this.value.hashCode();
  }

  // ...getCreated(), toString()
}
public class PostRepository {

  private Map<CacheKey, Post> cache;

  public PostRepository() {
    this.cache = new HashMap<>();
  }

  public Post getPostById(Integer id) {
    CacheKey key = new CacheKey(id); // CacheKey 인스턴스의 유효범위는 getPostById 메서드까지다.

    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() 메서드를 보면 id가 들어오면 CacheKey로 만들어 만약 cache에 저장되어 있으면 해당 객체를 반환하고, 없으면 해당 key에 해당하는 포스트를 넣는 작업을 한 후 post를 반환한다. 왜 Integer를 Map의 key로 사용하지 않았는지는 나중에 살펴보자. 여기서 주목할 점은 CacheKey의 유효범위가 getPostById 까지라는 것이다. 이유는 WeakHashMap을 사용할때 알아보자.

 

class PostRepositoryTest {

  @Test
  void cache() throws InterruptedException {
    PostRepository postRepository = new PostRepository();
    Integer p1 = 1;
    postRepository.getPostById(p1);

    assertFalse(postRepository.getCache().isEmpty());

    // TODO run gc
    System.out.println("run gc");
    System.gc();
    System.out.println("wait");
    Thread.sleep(3000L);
    
    assertTrue(postRepository.getCache().isEmpty());
  }
}

처음에는 캐시에 아무것도 없다가 postRepository.getPostById(p1);에 의해 Post 객체가 생성되고, cache에 들어가게 된다. 따라서 isNotEmpty()에서는 확인했을 때 잘 통과한다. 하지만 System.gc();을 통해서 GC를 실행하더라도 캐시가 비워지지 않기 때문에 마지막 isEmpty()에 대한 테스트가 실패하게 된다.

 

public PostRepository() {
  this.cache = new WeakHashMap<>();
}

위 코드처럼 WeakHashMap으로 변경하면 앞서 말했듯이 CacheKey 객체는 getPostById() 메서드 안에서만 있기 때문에 해당 메서드가 종료되면 바로 사라지고, key를 더 이상 참조하는 것이 없기 때문에 해당 엔트리를 WeakHashMap에서 해제해준다.

 

public Post getPostById(CacheKey key) {
  // ...
}

class PostRepositoryTest {

  @Test
  void cache() throws InterruptedException {
    PostRepository postRepository = new PostRepository();
    CacheKey key1 = new CacheKey(1);
    postRepository.getPostById(key1);

    // 생략...
   }
}

그리고 만약 CacheKey 객체를 인자로 받고, 해당 테스트 안에서 CacheKey 객체를 만들어 인자로 넘겨주면 테스트는 어떻게 될까? 이러한 경우에는 내가 아무리 WeakReference를 사용했지만 key를 참조하고 있는 변수가 있어 테스트가 실패하게 된다.

이러한 특징을 이용하는 방법이 메모리를 정리하는 방법 중 두 번째 방법이다.

 

메모리를 정리하는 세 번째 방법

세 번째로 추천하는 방법은 객체를 넣거나 빼거나 할 때 Map에서 제일 오래된 것을 찾아서 그때그때 삭제하는 방식으로 직접 관리하는 것이다. 또는 LRU(Least Recently Used) Cache 알고리즘, 즉 가장 최근에 사용된 몇 개까지만 캐시를 자료구조를 구현해 메모리를 관리할 수도 있다. LRU Cache를 이용하면 계속해서 수량을 정해둬 가장 자주 사용되는 수량만큼만 참조한 것만 남아있도록 만들 수 있다.

LRUCache를 이용해서 직접 메모리를 관리하는 방법에 대해서는 아래 글을 작성했으니 참고하면 좋다.

 

LRU 알고리즘 캐시 자료구조 만들기

LRU 알고리즘 LRU 알고리즘은 페이지 교체 알고리즘 중 하나로 사용되는 가장 오랫동안 참조되지 않은 페이지를 교체하는 기법을 말한다. 캐시가 사용하는 리소스의 양은 제한되어 있고, 캐시는

oneny.tistory.com

 

메모리를 관리하는 네 번째 방법(BackgroundThread)

public PostRepository() {
  this.cache = new HashMap<>();
}

다시 WeakHashMap 사용했던 것을 HashMap으로 돌려놓자.

 

class PostRepositoryTest {

  @Test
  void backgroundThread() throws InterruptedException {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    PostRepository postRepository = new PostRepository();
    CacheKey key1 = new CacheKey(1);
    postRepository.getPostById(key1);

    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());

    executor.scheduleAtFixedRate(removeOldCache,
            1, 3, TimeUnit.SECONDS);

    Thread.sleep(200000L);
    
    executor.shutdown();
  }
}

Background Thread를 이용해서 주기적으로 정리하는 작업는 Runnable 인터페이스의 run() 메서드를 람다식으로 사용하여 정의했다.

이에 대해 설명하자면 메모리를 정리하는 세 번째 방법으로 ScheduledExecutorService를 사용해서 호출될 때마다가 아닌 Background Thread에서 가장 오래된 key를 찾아서 해당 key를 해제해주는 작업을 처음에 1초 있다가 이후 매 3초마다 수행한다.

그러면 Thread.sleep(20000L);를 통해 애플리케이션을 20초 동안 돌리고 있는 동안 Background Thread가 매 3초마다 정리하는 작업을 수행해서 하나씩 삭제를 하는 것을 확인할 수 있다..

 

 

 

위 키워드들과 관련된 개념 정리

 

NullPointerException

Java 8 Optional을 활용해서 NPE를 최대한 피하자.

메서드에서 null를 리턴하거나 null 체크를 하지 않았기 때문에 NPE(NullPointerException)을 만나는 경우가 종종 있다. 메서드에서 적절한 값을 리턴할 수 없는 경우에는 1. 예외를 던지거나 2. null을 리턴하거나 3. Optional을 리턴하는 방법이 있다.

 

public class Channel {

  private int numOfSubscriber;

  public MemberShip defaultMemberShip() {
    if (this.numOfSubscriber < 2000) {
      return null;
    } else {
      return new MemberShip();
    }
  }
}
class ChannelTest {

  @Test
  void npe() {
    Channel channel = new Channel();
    MemberShip memberShip = channel.defaultMemberShip();
    memberShip.hello();
  }
}

위 보는 것처럼 메서드가 null을 리턴하는 경우가 있다. 이러면 사용하는 곳에서 null을 체크하지 않고 필드에 접근하거나 메서드를 호출하려고 할 때 NPE가 발생하게 된다. 

 

if (memberShip != null) {
  memberShip.hello();
}

따라서 이렇게 null이 아닌 경우에 메서드를 호출할 수 있도로 작성하여 간단하게 해결할 수 있다. 근데 defaultMemberShip() 메서드를 만드는 입장에서 NPE를 덜 만날 수 있게끔 Java 8부터 Optional을 이용해서 도와주는 방법이 있다.

 

public Optional<MemberShip> defaultMemberShip() {
  if (this.numOfSubscriber < 2000) {
    return Optional.empty();
  } else {
    return Optional.of(new MemberShip());
  }
}
@Test
void npe() {
  Channel channel = new Channel();
  Optional<MemberShip> memberShip = channel.defaultMemberShip();

  // memberShip.ifPresent(MemberShip::hello);
  memberShip.ifPresent(m -> {
    m.hello();
  });
}

위처럼 API를 제공하는 입장에서 Optional을 사용해서 반환하게 되면 사용하는 입장에서는 MemberShip 객체를 사용하기 위해서는 Optional에서 지원하는 기능을 사용해 Optional로부터 꺼내 해당 객체의 필드에 접근하거나 메서드를 호출할 수 있다. 이러면 이전에는 if문을 통해 분기처리를 해줘야 했는데 Optional에서 제공하는 기능을 통해 대체할 수 있고, 사용하는 입장에서 NPE 발생하기가 어려워진다.

 

WeakHashMap

더 이상 사용하지 않는 객체를 GC할 때 자동으로 삭제해주는 Map

WeakHashMap은 key가 더 이상 강하게 레퍼런스되는 곳이 없다면 해당 엔트리를 자동으로 제거한다. 레퍼런스의 종류는 다음과 같다.

  • Strong
  • Soft
  • Weak
  • Phantom

Map에 넣는 데이터의 중요성이 어디에 달려있는지가 중요한데 일반적으로 value가 유효할 때 key도 유효해야 하는 경우가 대부분이다. 이러한 경우에는 WeakHashMap을 사용하는 것이 적절치 않다. 반대의 경우, 즉 key에 의존해 key가 더 이상 유효하지 않거나 레퍼런스가 되는 곳이 없다면 value도 무의미해질 때 WeakHashMap을 사용하는 것이 좋다. 또한, 수동으로 비우지 않아도 되기 때문에 캐시를 구현하는데 활용할 수 있지만 캐시를 직접 구현하는 것은 권장하지 않는다.

 

WeakHashMap 주의사항

CacheKey key1 = new CacheKey(1);

위 예시에서 key를 Primitive 타입이나 String으로 하면 JVM 내부에서 일부의 값들이 캐싱이 된다. 그래서 key를 참조하고 있는 변수를 null로 만든다 하더라도 강하게 레퍼런스가 남아있다고 생각해 지워지지 않는다.

 

@Test
void cache() throws InterruptedException {
  PostRepository postRepository = new PostRepository();
  // CacheKey key1 = new CacheKey(1);
  Integer key = 1;
  postRepository.getPostById(key);

  assertFalse(postRepository.getCache().isEmpty());

  key = null;
  // TODO run gc
  System.out.println("run gc");
  System.gc();
  System.out.println("wait");
  Thread.sleep(3000L);

  assertTrue(postRepository.getCache().isEmpty());
}

따라서 위처럼 CacheKey를 key로 사용했던것을 Integer로 바꾸면 아무리 GC가 일어나도 JVM 내부 어딘간에 캐싱되어 있기 때문에 없어지지 않아 테스트를 통과하지 못한다.

 

SoftReference

public class SoftReferenceExample {

  public static void main(String[] args) throws InterruptedException {
    Object strong = new Object();
    SoftReference<Object> soft = new SoftReference<>(strong);
    strong = null;

    System.gc();
    Thread.sleep(3000L);

    // 메모리가 충분해서 굳이 제거할 필요가 없기 때문에 거의 안 없어진다.
    System.out.println(soft.get());
  }
}

SoftReferenceExample 결과

new SoftRerence<>(strong) 처럼 생성자를 호출할 때 SoftReference가 가리킬 인스턴스를 인자로 넘겨준다. 그러면 Object 객체를 Strong와 Soft가 레퍼런스하고 있다. 이 때, SoftReference의 특징으로 더 이상 Strong하게 레퍼런스하는 것이 없고, Soft하게만 레퍼런스한다면 해당 객체는 GC의 대상이 되는데 GC한다고 무조건 수거해가는 것이 아니라 메모리가 필요한 상황에  수거해간다. 따라서 위 예제코드는 SoftReference만이 레퍼런스하고 있지만 GC를 한다고 해서 없어지지 않고 메모리 공간이 충분하기 때문에 없어지지 않는다.

 

WeakReference

public class WeakReferenceExample {

  public static void main(String[] args) throws InterruptedException {
    Object strong = new Object();
    WeakReference<Object> weak = new WeakReference<>(strong);
    strong = null;

    System.gc();
    Thread.sleep(3000L);

    // WeakReference 경우에는 거의 없어진다.
    System.out.println(weak.get());
  }
}

WeakReferenceExample 결과

WeakReference는 SoftReference와 다르게 GC가 실행될때 메모리 공간이 필요한지 여부와 상관없이 무조건 없어진다. 

 

WeakReference 주의사항

public class ChatRoom {

  private List<WeakReference<User>> users;

  public ChatRoom() {
    this.users = new ArrayList<>();
  }

  public void addUser(User user) {
    this.users.add(new WeakReference<>(user));
  }

  public void sendMessage(String message) {
    users.forEach(wr -> Objects.requireNonNull(wr.get()).receive(message));
  }

  public List<WeakReference<User>> getUsers() {
    return users;
  }
}
class ChatRoomTest {

  @Test
  void chatRoom() throws InterruptedException {
    ChatRoom chatRoom = new ChatRoom();
    User user1 = new User();
    User user2 = new User();

    chatRoom.addUser(user1);
    chatRoom.addUser(user2);

    chatRoom.sendMessage("hello");

    user1 = null; // chatRoom의 List 객체에 담긴 user1은 GC가 될까?

    System.gc();

    Thread.sleep(5000L);

    List<WeakReference<User>> users = chatRoom.getUsers();

    assertThat(users).hasSize(1);
  }
}

WeakReference<User> 객체들이 더 이상 참조를 하지 않으면 List에서 사라질 것이라고 예상했다. 즉, 테스트 코드를 보면 user1을 null 만들어 참조하는 것을 없게 만들면 User 객체의 0번 인덱스가 사라질 것 같다고 예상했다. 하지만 예상과 달리 없어지지 않아 WeakReference의 객체가 List에 남아 테스트가 실패한 것을 확인할 수 있다.

이전에 봤던 WeakReference를 삭제해주는 기능은 WeakHashMap에 들어있던 기능이다. 그래서 WeakReference가 참조하고 있는 객체가 없어 그 자체도 없애고 싶다면 List를 커스텀해서 그 기능을 만들어야 한다.

 

PhantomReference

public class BigObjectReference<BigObject> extends PhantomReference<BigObject> {

  public BigObjectReference(BigObject referent, ReferenceQueue<? super BigObject> q) {
    super(referent, q);
  }

  public void cleanUp() {
    System.out.println("clean up");
  }
}
public class PhantomReferenceExample {

  public static void main(String[] args) throws InterruptedException {
    BigObject strong = new BigObject();
    ReferenceQueue<BigObject> rq = new ReferenceQueue<>();

    BigObjectReference<BigObject> phantom = new BigObjectReference<>(strong, rq);
    System.gc();
    Thread.sleep(3000L);


    // 팬텀은 유령이기 때문에 죽었지만 사라지진 않고 큐에 들어간다.
    System.out.println(phantom.enqueue()); // strong이 사라져 queue에 들어갔는지 확인 가능

    Reference<? extends BigObject> reference = rq.poll();
    BigObjectReference bigObjectCleaner = (BigObjectReference) reference;
    bigObjectCleaner.cleanUp(); // 자원반납 메서드 실행
    reference.clear(); // 직접 phantomReference 객체 해제
  }
}

PhantomReference는 SoftReference와 WeakReference와 다르게 지워주는 것이 아니라 Strong한 인스턴스가 참조하는 것이 없으면 PhantomReference에 남아있다고 볼 수 있다. PhantomReference에만 남은 경우에 GC가 일어나면 본래 가지고 있던 객체는 정리를 하고, PhatomReference 인스턴스(phantom)를 ReferenceQueue에 넣어준다. 그리고 나중에 Queue에 꺼내서 정리를 할 수 있다.

 

PhantomReference는 크게 2가지 용도가 있는데 자원을 정리하기 위해서와 언제 무거운 객체가 메모리 해제되는지 알 수 있다. strong이 사라짐과 동시에 rq에 들어가기 때문에 해당 strong한 객체가 rq에 들어갔는지 확인하면 메모리 해제되는 시점을 알 수 있다.

하지만 자원을 반납하기 위해서 PhantomReference를 사용하기 보다는 try-with-resources를 사용하는 것이 좋다.

 

ScheduledThreadPoolExecutor

public class ExecutorExample {

  public static void main(String[] args) throws ExecutionException, InterruptedException {

    for (int i = 0; i < 100; i++) {
      Thread thread = new Thread(new Task());

      thread.start();
    }

    System.out.println(Thread.currentThread() + " hello");
  }

  static class Task implements Runnable {
    @Override
    public void run()  {
      try {
        Thread.sleep(2000L);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      System.out.println(Thread.currentThread() + " world");
    }
  }
}

스레드 만드는 것 자체가 시스템 리소스를 많이 사용한다. 위 출력된 결과를 보면 알 수 있듯이 100개의 스레드를 생성하는 것은 굉장히 시스템 리소스를 사용한 것이다. 만약에 스레드 100개를 사용하지 않고도 비동기적으로 100개의 task를 별도의 스레드로 다 처리할 수 있게 만들 수 있을까? ThreadPool을 이용하면 된다. 

 

public class ExecutorExample {

  public static void main(String[] args) throws ExecutionException, InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(10);
    
    Future<String> submit = service.submit(new Task());

    System.out.println(Thread.currentThread() + " hello");

    System.out.println(submit.get()); // get 메서드를 사용하면 blocking이 된다.

    service.shutdown();
  }

  static class Task implements Callable<String> {
    @Override
    public String call() throws Exception {
      Thread.sleep(2000L);
      return Thread.currentThread() + " world";
    }
  }
}

위에서 보는 것처럼 ThreadPool의 개수를 조정할 때 CPU에 집중적인 작업인지 I/O에 집중적인 작업인지 2가지에 대해 신경을 써야한다. CPU에 집중적인 작업이라면 아무리 Thread 개수를 늘려도 CPU 개수를 넘어가면 어차피 막히기 때문에 나머지 스레드는 기다려야 한다. 그래서 CPU에 집중적인 작업이라면 CPU의 개수만큼만 만드는 것이 좋다.

 

CPU의 개수는 Runtime.getRuntime().availableProcessors()를 통해서 알 수 있다. 그리고 이를 통해 ThreadPool을 만드는 방법은 다음과 같다.

  • Executors.newSingleThreadExecutor(): 스레드 개수가 하나이다.
  • Executors.newFixedThreadPool(int nThreads): 개수를 고정한다.
  • Executors.newCachedThreadPool(): 자기가 필요한만큼 스레드를 만든다. 기존에 스레드가 있으면 놀고 있는 스레드를 재사용하고, 모두 작업을 하는 중이면 하나 새로 생성한다. 그리고 오랫동안 아무 작업을 하지 않은 스레드가 있으면 자동으로 없애준다.
  • Executors.newScheduledThreadPool(int corePoolSize): Schedule을 감안해서 순서가 조금 바뀔 수 있다. 어떤 작업을 딜레이하거나 주기적으로 실행하는 경우 등 특별한 용도로 사용된다.

 

I/O에 집중적인 작업 같은 경우에는 input, output을 처리하기 위해서 딜레이가 발생하는데 만약 스레드가 개수를 적게 잡으면 I/O를 처리한다고 모두 스레드를 작업을 하지만 I/O 처리하는 작업 때문에 CPU 리소스는 남는 경우가 있다. 따라서 I/O에 집중적인 작업인 경우에는 스레드 개수를 CPU의 개수 보다는 많이 잡는 경우가 많고, 적절한 개수는 그때마다 다르다.

 

Callable

별도의 스레드가 수행된 후 결과를 리턴받고 싶을 경우 사용한다. 

 

 

마지막 핵심 정리

  • 어떤 객체에 대한 레퍼런스가 남아있다면 해당 객체는 가비지 컬렉션의 대상이 되지 않는다.
  • 자기 메모리를 직접 관리하는 클래스라면 메모리 누수에 주의해야 한다.
    • 예) 스택, 캐시, 리스너 또는 콜백
  • 참조 객체를 null 처리하는 일은 예외적인 경우이며 가장 좋은 방법은 유효 범위 밖으로 밀어내는 것이다.

 

출처

이펙티브 자바 Effective Java 3/E - 아이템 7. 다 쓴 객체 참조를 해제하라

이펙티브 자바 완벽 공략 1부

 

 

 

 

 

 

 

 

'Java > Java' 카테고리의 다른 글

JVM 메모리 구조  (0) 2023.08.09
LRU 알고리즘 이용한 메모리 직접 관리하는 In-memory 캐시  (0) 2023.08.06
표준과 구현  (0) 2023.07.31
JMH로 warm up 후 테스트하기  (0) 2023.07.31
Object 클래스의 clone() 메서드  (0) 2023.07.30