ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템7. 다 쓴 객체 참조를 해제하라.
    Java/이펙티브 자바 2023. 6. 1. 15:40

    핵심정리

    - 어떤 객체에 대한 레퍼펀스가 남아있다면 해당 객체는 가비지 컬렉션의 대상이 되지 않는다.

    - 자기 메모리를 직접 관리하는 클래스라면 메모리 누수에 주의해야 한다.

        ㆍ예) 스택, 캐시, 리스너 또는 콜백

     

    첫번째, 참조 객체를 null 처리하는 일은 예외적인 경우이며 가장 좋은 방법은 유효 범위 밖으로 밀어내는 것이다.

    public Object pop() {
        if(size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }

     

    두번째, WeakHashMap 을 사용하는 경우 (특정한 자료구조를 사용)

    1.

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

    - 인스턴스의 유효기간은 그 메서드 안에서만 이다.

    - 메서드가 끝나는 순간 유효시간이 없어진다.

     

    2.

    @Test
    void cache() throws InterruptedException {
        PostRepository postRepository = new PostRepository();
        CacheKey key = new CacheKey(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());
    }
    public Post getPostById(CacheKey key) {
    
        if ( cache.containsKey(key)) {
            return cache.get(key);
        }else {
            //TODO DB에서 읽어오거나 REST API를 통해서 읽어올 수 있다.
            Post post = new Post();
            cache.put(key, post);
            return post;
        }
    }

    - 명시적으로 key를 null 처리로 참조를 없앰

     

     

    세번째, 객체를 넣거나 빼거나 할 때 직접 관리

    - 오랜된 것은 삭제, 최근에 사용한 몇 개까지만 캐시하겠다.

     

    네번째, 백그라운드 스레드를 사용하여 주기적으로 클린업

    @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(20000L);
    
        executor.shutdown();
    }

    완벽공략

    - p37, NullPointerException

    - p38, WeakHashMap

        ㆍp38, 약한 참조 (weak reference)

    - p39, 백그라운드 쓰레드

        ㆍp39, ScheduledThreadPoolExecutor

     

    완벽공략19 - NullPointerException

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

    - NullPointerException을 만나는 이유

        ㆍ메소드에서 null을 리턴하기 때문에 && null 체크를 하지 않았기 때문에

    - 메소드에서 적절한 값을 리턴할 수 없는 경우에 선택할 수 있는 대안

        ㆍ예외를 던진다.

        ㆍnull을 리턴한다.

        ㆍOptional을 리턴한다.

     

    - optional를 리턴 타입으로만 쓴다.

    - optional로 list나 set를 감싸지마라. 그것 자체로 비어있는 없는지 확인하는 메소드가 있다.

    - primitive 타입은 optionalInt, optionalLong을 사용한다

     

    public class Channel {
        private int numOfSubscribers;
    
    //    public MemberShip defaultMemberShip() {
    //        if(this.numOfSubscribers < 2000) {
    ////            return null;
    //            throw new IllegalStateException();
    //        }else {
    //            return new MemberShip();
    //        }
    //    }
    
        public Optional<MemberShip> defaultMemberShip() {
           if(this.numOfSubscribers < 2000) {
               return Optional.empty();
           }else {
               return Optional.of(new MemberShip());
           }
        }
    }
    @Test
    void npe() {
        Channel channel = new Channel();
        Optional<MemberShip> optional = channel.defaultMemberShip();
    //        if(memberShip != null) {
    //            memberShip.equals(new MemberShip());
    //        }
        optional.ifPresent(m -> {
            m.hello();
        });
    
        MemberShip memberShip = optional.get(); // 값이 없다면 NoSuchElementException
    }

     

    완벽공략20 - WeakHashMap

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

    - Key가 더이상 강하게 레퍼런스되는 곳이 없다면 해당 엔트리를 제거한다.

    - 레퍼런스 종류

        ㆍStrong, Soft, Weak, Phantom

    - 맵의 엔트리를 맵의 Value가 아니라 Key에 의존해야 하는 경우에 사용할 수 있다.

    - 캐시를 구현하는데 사용할 수 있지만, 캐시를 직접 구현하는 것은 권장하지 않는다.

     

    - Custom 한 레퍼런스인 경우 WeakHashMap을 쓰는 것은 괜찮지만 Wrapper type을 쓸 때는 JVM 내부에 캐시되었기에 없어지지 않는다.

    - reference

        ㆍStrong : 평소에 할당할 때 사용, 해당 메소드 안에서 && 참조할때가 없을 때 ( null 로 만들어 줄 때 )

    public List<WeakReference<User>> getUsers() {
        ChatRoom chatRoom = new ChatRoom();
        chatRoom = null;
        return users;
    }

        ㆍSoft : Strong 레퍼런스를 하지않고 Soft 레퍼런스만 하고 있다면 메모리가 필요한 상황에 GC를 해준다.

    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);
    
        //TODO 거의 안 없어진다.
        // 왜냐면 메모리가 충분해서.. 굳이 제거할 필요가 없으니깐
        System.out.println(soft.get());
    }

        ㆍWeak : GC가 일어날때 무조건 없어진다.

    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);
    
        // TODO 거의 없어진다.
        // 약하니깐
        System.out.println(weak.get());
    }

        ㆍPhantom : Phantom 레퍼런스가 Strong 대신에 남아있다. ReferenceQueue에 남아있다.

                             1. 자원정리 / 2. 언제 객체가 메모리 해제되는지 알 수 있다. ( 큐를 확인 )

    public static void main(String[] args) throws InterruptedException {
            BigObject strong = new BigObject();
            ReferenceQueue<BigObject> rq = new ReferenceQueue<>();
    
            BigObjectReference<BigObject> phantom = new BigObjectReference<>(strong, rq);
    //        PhantomReference<BigObject> phantom = new PhantomReference<>(strong, rq);
            strong = null;
    
            System.gc();
            Thread.sleep(3000L);
    
            // TODO 팬텀은 유령이니깐
            // 죽었지만... 사라지진 않고 큐에 돌아간다.
            System.out.println(phantom.isEnqueued());
    
            Reference<? extends BigObject> reference = rq.poll();
            BigObjectReference bigObjectCleaner = (BigObjectReference) reference;
            bigObjectCleaner.cleanUp();
            reference.clear();
        }
    public class BigObjectReference<BigObject> extends PhantomReference<BigObject> {
    
        /**
         * Creates a new phantom reference that refers to the given object and
         * is registered with the given queue.
         *
         * <p> It is possible to create a phantom reference with a {@code null}
         * queue, but such a reference is completely useless: Its {@code get}
         * method will always return {@code null} and, since it does not have a queue,
         * it will never be enqueued.
         *
         * @param referent the object the new phantom reference will refer to
         * @param q        the queue with which the reference is to be registered,
         *                 or {@code null} if registration is not required
         */
        public BigObjectReference(BigObject referent, ReferenceQueue<? super BigObject> q) {
            super(referent, q);
        }
    
        public void cleanUp() {
            System.out.println("clean up"); // 여기서 자원반납
        }
    }

     

    * 기선쌤 : weak, soft 레퍼런스로 CG로 객체를 없애는 건 불확실하다. 권장 X

                    불필요한 상황에서 없애주는 게 더 명시적이다.

                    phantom도 권장하지 않는다. - 큰 오브젝트가 언제 사라지는가 확인할 때는 사용하기 괜찮음

                   weak, soft, phantom 을 쓰는 경우는 극히 드물다.

    완벽공략21 - ScheduledThreadPoolExecutor

    : Thread와 Runnable을 학습했다면 그 다음은 Executor. 

    Thread, Runnable, ExecutorService

    public class ExecutorsExample {
    
        public static void main(String[] args) {
            ExecutorService service = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 100; i++) {
    //            Thread thread = new Thread(new Task());
    //            thread.start();
                service.submit(new Task());
            }
    
            System.out.println(Thread.currentThread() + " hello");
    
            service.shutdown();
        }
    
        private static class Task implements Runnable {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + " world");
            }
        }
    }

        ㆍ쓰레드를 계속 만드는 것이 아니라 풀을 만들어서 쓰레드의 갯수만큼만 쓰게한다.

    쓰레드풀의 개수를 정할 때 주의할 것

        ㆍCPU, I/O

        ㆍCPU를 많이 쓰는 작업은 CPU의 개수를 넘어가면 막힌다. 결국엔 CPU 갯수만큼만 만들 수 있다. 

        int numberOfCpu = Runtime.getRuntime().availableProcessors();   // cpu 갯수

        ㆍI/O

            ○ 응답 시간에 따른 적절한 쓰레드 갯수를 지정해야 한다.

    쓰레드툴의 종류

        ㆍSingle, Fixed, Cached, Scheduled

    Single

    ExecutorService service = Executors.newSingleThreadExecutor();   // 쓰레드 하나가지고 모든 작업을 처리

    - 쓰레드 하나가지고 모든 작업을 처리

    Fixed

    int numberOfCpu = Runtime.getRuntime().availableProcessors();   // cpu 갯수
    ExecutorService service = Executors.newFixedThreadPool(numberOfCpu);

    - 해당 개수만큼 쓰레드를 만들고 사용

    - 블럭킹 큐( 쓰레드들이 동시에 접근가능) 사용

    Cached

    ExecutorService service1 = Executors.newCachedThreadPool();       // 필요한 만큼 쓰레드를 만든다. - 쓰레드가 무한정 늘어날 수 있다.

    - 작업 공간이 하나.

    - 필요한 만큼 쓰레드를 만든다.

    - 쓰레드가 무한정 늘어날 수 있다.

    Scheduled

    ExecutorService service = Executors.newScheduledThreadPool(10);    // 입력이 순차적이라고 순차적 작업 X

    - 순차적으로 들온다고 해서 작업이 순차적이지 않다. 순서가 바뀔 수 있다.

    - 어떤 작업을 몇 초 뒤에 실행하거나 주기적으로 실행할 때 쓰는 특별한 용도의 쓰레드

     

    Runnable, Callable, Future

    Runnable

    - 리턴타입이 없다.

     

    Callable, Future

    - 작업에 대한 결과를 받고 싶을 때 사용

    public static void main(String[] args) {
            int numberOfCpu = Runtime.getRuntime().availableProcessors();   // cpu 갯수
            ExecutorService service = Executors.newFixedThreadPool(numberOfCpu);
    
            Future<String> submit = service.submit(new Task());
    
            System.out.println(Thread.currentThread() + " hello");
    
            System.out.println(submit.get());	// 결과를 기다린다.
    
            service.shutdown();
    }
    
    private static class Task implements Callable<String> {
    
        @Override
        public String call() throws Exception {
            Thread.sleep(2000L);
            return Thread.currentThread() + " world";
        }
    }

    - submit.get() 호출 이후 블럭킹 ( 2초 후 작업 )

     

    CompletableFuture, ForkJoinPool

    댓글

Designed by Tistory.