반응형

finalizer와 cleaner 사용을 피하라

finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.

cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

finalizer와 cleanser는 즉시 수행된다는 보장이 없다. 객체에 접근 할 수 없게 된 후 finalizer나 cleaner가 실행되기까지
얼마나 걸릴지 알 수 없다.
finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.

finalizer와 cleaner는 심각한 성능 문제도 동반한다.

finalize가 가비지 컬렉터의 효율을 떨어뜨리기 때문이다.

객체 생성을 막으려면 생성자에서 예외를 던지는 것만드로 충분하지만, finalize가 있다면 그렇지 않다.

final 클래스들은 그 누구도 하위 클래스를 만들 수 없으니 이 공격에서 안전함

final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.

AutoCloaseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 됨

cleaner와 finalizer를 적절히 활용하는 두 번째 예는 네이티브 피어(native peer)와 연결된 객체에서다.

cleaner를 안정망으로 활용하는 AutoCloseable 클래스

public class Room implements AutoCloseable {
    private static final Cleaner cleaner= Cleaner.create();

    private static class State implements Runnable {
        int numJunkPiles;

        State(int numJunkPiles){
            this.numJunkPiles = numJunkPiles;
        }

        @Override public void run(){
            System.out.println("방 청소");
            numJunkPiles = 0;
        }
    }

    private final State state;

    private final Cleaner.Cleanable cleanable;

    public Room(int numJunkPiles){
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }

    @Override public void close(){
        cleanable.clean();
    }

}

모든 Room 생성을 try-with-resources 블록으로 감싼다면 자동 청소는 전혀 필요하지 않음

public class Adult{
    public static void main(String[] args) {
        try(Room myRoom = new Room(7)){
            System.out.println("안녕~");
        }
    }
}

방 청소를 하지 않는 프로그램

public class Teenager {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("아무렴");
    }
}

정리

cleaner는 안정망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자,
물론 이런 경우라도 불확실성과 성능 저하에 주의해야 한다.

728x90
반응형

1. 스택

스택을 간단히 구현한 코드

메모리 누수가 일어나는 위치가 어디인가?

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

}

스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것임
스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않음
스택이 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문임

해법

pop() 메서드 부분을 해당 참조를 다 썼을 때 null 처리(참조 해제) 하면된다.

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

    Object result = elements[--size];
    elements[size] = null;

    return result;
}

만약 null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지면 종료

객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수를 주의해야 함

2. 캐시

캐시 역시 메모리 누수를 일으키는 주범이다.
캐시 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만들자

3. 리스너 혹은 콜백

클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백을 계속 쌓여갈 것임
콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해감

정리

메모리 누스는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 함
그래서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요

728x90
반응형
String s = new String("bikini");

실행될 때마다 String 인스턴스를 새로 만듬

String s = "bikini";

다음 개선된 버전은 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용함

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

Boolean(String);

//대신        
Boolean.valueOf(String);

성능을 훨씬 더 끌어올릴 수 있다!

static boolean isRomanNumeral(String s){
    return s.matches("[정규식]");
}

String.matches는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 사황에서 반복해 사용하기엔 적합하지 않음

Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드가 호출될 때마다 인스턴스를 재사용

값비싼 객체를 재사용해 성능을 개선한다.

public class RomanNumerals {
    private static final Pattern ROMAN = Pattern.compile("[정규식]");

    static boolean isRomanNumeral(String s){
        return ROMAN.matcher(s).matches();
    }
}

1.1ms -> 0.17ms 6.5배 빨라짐
성능만 좋아진 것이 아니라 코드도 더 명확해짐

어댑터 (패턴)

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.
어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 씀

GoF의 어댑터 패턴 UML

어댑터는 뒷단 객체만 관리하면된다. 즉, 뒷단 객체 외에는 관리할 상태가 없으므로 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분하다.
어댑터를 뷰라고 부름

Map 인터페이스의 keySet 메서드
keySet이 뷰 객체를 여려 개 만들어도 상관은 없지만 그럴 필요도 없고 이득도 없음

오토박싱(AutoBoxing)

기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아님
끔찍이 느리다! 객체가 만들어지는 위치를 찾았는가?

private static long sum(){
    Long sum = 0L;
    for(long i=0; i <= Integer.MAX_VALUE; i++)
        sum += i;

    return sum;
}
  • Long으로 선언해서 불필요한 Long 인스턴스가 약 231개나 만들어진 것
  • sum변수의 타입을 long으로만 바꿔주면 내 컴퓨터에서는 6.3초에서 0.59초로 빨라진다.
  • 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.

정리

  • 객체 생성은 비싸니 피해야 한다로만 생각하면 안됨
  • 방어적 복사에 실패하면 언제 터져 나올지 모르는 버그와 보안 구멍으로 이어지지만,
  • 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 줌

얕은 복사, 방어적 복사, 깊은 복사

  • 얕은 복사 : 객체를 복사할 때, 객체의 주소 값만을 복사하는 방식
  • 방어적 복사 : 객체의 주소를 복사하지 않고 객체의 내부 값을 참조하여 복사하는 방법
  • 깊은 복사 : 객체의 모든 내부 상태를 완전히 복사하여 새로운 객체를 만드는 방법
728x90

+ Recent posts