■ String 리터럴을 이용한 불필요한 객체 생성 방지
똑같은 기능의 객체를 매번 생성하지 말고 객체 하나를 재사용하는 편이 나을 때가 많다. 특히 불변 객체는 언제든 재사용할 수 있다. String의 경우에 이를 한번 생각해 볼 수 있다.
String의 두가지 생성 방식에 대해서 간단히 알아보겠다.
1. new 연산자를 이용한 방식
2. 리터럴을 이용한 방식
new를 이용하여 String을 생성하면 Heap 영역에 존재하게 된다. 그리고 리터럴을 이용할 경우 String pool 이라는 영역에 존재하게 된다. 이를 이용하여 단 하나의 문자열 객체만을 생성하고 캐싱할 수 있다.
String a = "Hello";
String b = "Hello";
String c = new String("Hello");
System.out.println(a==b); //true
System.out.println(a==c); //false
"==" 연산으로 비교할 경우 값을 비교하는게 아니라 같은 메모리를 참조하는지를 비교한다.
- a의 경우 Hello 라는 문자열을 String pool에 넣게 된다.
- b의 경우는 이미 같은 문자열이 String pool에 존재하기 때문에 같은 값을 참조하게 된다.
- c의 경우에는 new 연산자를 사용하여 새로운 객체를 생성했기 때문에 String pool이 아닌 다른 주소값을 참조하게 된다.
String pool의 위치는 Java 6까지는 JVM Heap 내부의 PermGen 영역에 있다. String pool에 문자열 객체가 많이 생성되거나 다른 이유로 이 영역이 가득 차게 되면 OutOfMemory 에러가 발생하였다. PermGen 영역의 경우 객체가 가득 차더라도 Runtime(실행중) 환경에서는 동적으로 메모리를 증가시킬 수 없기 때문이다.
Java7 이후로는 다른 일반 객체들과 마찬가지로 Perm 영역이 아닌 Heap에 String Pool을 생성한다.
■ 정적 팩터리 메서드를 이용한 불필요한 객체 생성 방지
생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다. Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.
Boolean boolean1 = Boolean.valueOf("true");
Boolean의 생성자는 Java 9에서 deprecated api로 지정되었다. 불변 객체뿐만 아니라 가변 객체라 해도 사용중에 변경되지 않는다면 재사용이 가능하다.
■ 캐싱을 이용한 불필요한 객체 생성 방지
생성 비용이 아주 비싼 객체도 있다. 이런 비싼 객체가 반복해서 필요할 경우 캐싱하여 재사용하길 권한다. 하지만 자신이 만드는 객체가 비싼 객체인지를 매번 명확히 알 수 없다는 문제가 있다.
다음은 주어진 문자열이 유효한 로마 숫자인지를 확인하는 메서드이다. 이 코드의 문제는 String.matches 메서드를 사용한다는 것이다. String.matches는 정규 표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지않다.
static boolean isRomanNumeralSlow(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
이 메서드가 내부에서 만드는 정규 표현식용 Pattern 인스턴스는 한번 쓰고 버려져서 바로 가비지 컬렉션 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신을 만드기 때문에 인스턴스 생성 비용이 높다.
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
성능 개선을 위해서는 필요한 정규 표현식을 불변의 Pattern 인스턴스로 클래스 초기화(정적 초기화) 과정에서 직접 생성해서 캐싱해두고 재사용하는 것 이다.
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeralFast(String s) {
return ROMAN.matcher(s).matches();
}
}
■ 기본 타입을 이용한 불필요한 객체 생성 방지
오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.
오토 박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다. 의미상으로는 별다를 것 없지만 성능상으로는 그렇지 않다. long 타입의 i를 Long 타입인 sum에 더할 경우 불필요한 Long 인스턴스가 생성되고 자동으로 형변환이 된다. Long을 long으로만 바꿔도 훨씬 빨라진다.
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
■ 오해하면 안되는점
- 객체 생성은 비싸니 피해야한다로 오해하지말자. 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일은 크게 부담되지 않는다. 프로그램의 명확성, 간결성, 기능을 위해서 객체를 생성하는 것이라면 일반적으로 좋은 일이다.
- 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 여러분만의 객체 풀을 만들지 말자. JVM의 가비지 컬렉터는 상당히 잘 최적화 되어서 가벼운 객체를 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다
REFERENCE
- 조슈아 블로크, Effective Java
- dololak.tistory.com/718
- jithub.tistory.com/309
- coding-factory.tistory.com/547
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템8 finalizer와 cleaner 사용을 피하라 (0) | 2021.01.21 |
---|---|
[Effective Java] 아이템7 다 쓴 객체 참조를 해제하라 (0) | 2021.01.17 |
[Effective Java] 아이템5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2021.01.14 |
[Effective Java] 아이템4 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2021.01.13 |
[Effective Java] 아이템3 private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2021.01.13 |