본문 바로가기

0. Intro

  자바에서는 기본형 타입을 Wrapping한 Wrapper클래스를 제공한다. Wrapper클래스에 대해서 간단히 알아보고 Wrapper클래스의 문제점 Wrapper클래스의 성능을 조사한 글이다. 글의 아래 순서로 구성되어 있다.

 

1. Wrapper클래스 소개

2. Wrapper클래스 사용 주의사항

3. Wrapper클래스 성능

4. 정리

 

 

 

1. Wrapper클래스 소개

 

1.1 박싱과 언박싱

  자바 기본형 타입을 참조형 타입으로 사용하기 위해 Wrapper 클래스를 제공해준다. jdk 1.5부터는 기본형을 참조형으로 자동으로 변환해주는 박싱, 참조형을 기본형으로 자동변환해주는 언박싱을 지원해준다.

Integer n1 = 1;
Integer n1 = new Integer(1);
  
int n2 = n1;
int n2 = n1.intValue();

 

   아래와 같이 코드를 작성하면

Integer n1 = 1;

  컴파일 하면 아래와 같은 코드로 나온다.

Integer n1 = Integer.valueOf(1);

 

 

 

1.2 기본형,참조형 차이점

  1. 식별성(Identitiy)
    1. 박싱된 wrapper클래스는 값이 같아도 서로 다르다고 식별할 수 있다.
    2. 기본형에서 같은값은 언제나 같다.
  2. Nullable
    1. 박싱된 wrapper클래스는 null을 가질 수 있다.
    2. 기본형은 언제나 기본값이 들어간다.
  3. 메모리
    1. 박싱된 wrapper클래스는 heap영역에 올라간다.

 

 

 

2. Wrapper클래스 주의사항

 

2.1 ==사용시 문제점

 

Integer a=1000, b=1000;
System.out.println(a == b); //false
 
 
Integer c=100, d=100;
System.out.println(c == d); //true

 

//프로그램코드에서 아래 코드를 실행하면,
Integer i = 100;
 
 
//내부적으로, 아래와 같이 컴파일러에서 처리
Integer i = Integer.valueOf(100);

 

  Integer클래스에서는 IntegerCache라는 클래스가 있다.

private static class IntegerCache
{
    private IntegerCache(){}
    static final Integer cache[] = new Integer[-(-128) + 127 + 1];
    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Integer(i - 128);
    }
}

 

  Integer클래스의 valuOf메서드를 사용하거나 정수형 리터럴을 대입해서 Integer클래스로 오토박싱되도록 하면 내부에서는 IntegerCace의 캐싱된 값들을 사용한다.

public static Integer valueOf(int i)
{
    final int offset = 128;
    if (i >= -128 && i <= 127) // must cache
        return IntegerCache.cache[i + offset];
    }
    return new Integer(i);
}

 

 

  아래에서 확인할 수 있듯이, valueOf을 사용했거나 기본형값을 대입했으면 메모리에 이미 캐싱된 값을 사용하고 있지만, new인스턴스로 새롭게 Integer를 생성한다면 캐싱된값이 아닌 새로운 객체를 생성하는것을 확인할 수 있다.

public static void main(String[] args) {
 
        Integer a1 = 100;
        Integer a2 = 100;
        Integer a3 = new Integer(100);
 
        System.out.println(a1 == a2); //true
        System.out.println(a1 == a3); // false
}

 

 

2.2  null 에러

  아래와 같은 코드는 NullPointerException을 던진다. if문에서 i==42 라고 적힌 코드는 참조형과 기본형 데이터를 비교하는것이다. 기본형과 wrapper클래스를 혼용해서 사용하는 연산에서는 wrapper클래스가 언박싱된다. 그리고 i라는 Integer Wrapper클래스는 null값이 들어가있으므로 null을 언박싱하려다가 NullPointerException을 맞이하게 된다.

 

public class Test {
	static Integer i;

	public static void main(String [] args) {
		if(i == 42) {
				System.out.println("Hello");
		}
	}
}

 

  해결방법은 간단하다 기본형 타입으로 i를 선언하면 끝.!!

public class Test {
	static int i;

	public static void main(String [] args) {
		if(i == 42) {
				System.out.println("Hello");
		}
	}
}

 

 

 

2.3 다른 Wrapper클래스들

  • Boolean 객체는 True,False값을 캐싱
  • Character객체는 유니코드 0~127값을 캐싱
  • Long,Integer객체는 -128~128값을 캐싱
  • Float,Double객체는 캐싱을 제공하지 않음

 

 

 

3. Wrapper클래스 성능

 

 

3.1 Wrapper클래스 연산 성능

 

  Wrapper클래스를 이용해서 연산했을때와 기본형값을 이용해서 연산을 수행했을때의 성능을 비교해보자. 아래 코드는 먼저 Wrapper클래스를 사용해서 연산하는 코드이다. 

long start = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("time consuming:"+(end-start)/1000.0);

// Output:
// 2305843005992468481
// Time: 15.175

 

3.2 기본형타입 연산 성능

  다음은, 기본형타입만 이용한 연산이다. Wrapper클래스를 사용했을때보다 성능이 훨씬 좋다. 

long start = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i; // Add using basic data types
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("time consuming:"+(end-start)/1000.0);

// Output:
// 2305843005992468481
// Time: 1.643

 

3.3 성능차이가 발생하는 이유

  이런 현상이 생기는 이유는 뭘까? 이제 성능차이가 발생하는 이유를 알아보자. 박싱과 언박싱이 빈번하게 일어나면서 성능에 문제가 생긴다. 박싱은 Wrapper클래스를 heap영역에 올린다. 반복해서 heap영역에 객체 인스턴스를 생성시키면 heap영역에 단편화(Fragmentation) 발생한다.

 

※ 단편화 : 메모리가 할당되고 해제되는 작업이 반복될 때 작은 메모리가 중간중간에 있는데 중간중간에 생긴 사용하지 않는 메모리가 많이 존재해서 총 메모리공간은 충분하지만 실제로 할당할 수 없는 상황

Integer a = 5;
a+=1;

 

  아래는 위의 코드를 실행할때의 바이트코드이다. 가장먼저 Integer a = 5 코드에서 오토박싱이 일어나면서 5라는 기본형값을 Integer.valueOf()을 통해서 Wrapper클래스로 만들어준다. 다음은 Wrapper클래스인 a값을 Integer.intValue()를 통해서 기본형타입으로 언박싱시킨다. 기본형타입으로 5 + 1을 진행하고나서 다시 Integer.valueOf()을 통해서 박싱시킨다.

 0 iconst_5
 1 invokestatic #16 <java/lang/Integer.valueOf>
 4 astore_1
 5 aload_1
 6 invokevirtual #22 <java/lang/Integer.intValue>
 9 iconst_1
10 iadd
11 invokestatic #16 <java/lang/Integer.valueOf>
14 astore_

 

 

  아래는 VisualVM이라는 도구를 이용해서 Heap사용량을 측정해본 결과이다. Wrapper클래스를 사용하고 Wrapper이용해서 연산을 수행할경우 박싱과 언박싱이 일어나면서 Heap영역의 사용률이 높아지는것을 확인할 수 있다.

 

 

  힙영역은 GC에 의해서만 수거가 되는데 메모리를 정리하는 단계에서 메모리 단편화가 발생한다(Memory Fragmentation)  메모리 단편화가 발생하면 어떤 문제가 생기는걸가? 먼저, 힙에 새로 올릴려는 객체 인스턴스에 적합한 여유공간을 가지는 메모리를 찾기 위해서 더 많은 시간을 소요해야 한다. 또한, 객체가 메모리에 올라갈때 점유하는 메모리 공간은 반드시 연속적이어야 한다. 단편화가 심해지면 전체 가용공간이 남아있지만 연속적인 공간이 없어서 인스턴스 생성에 실패할 수 있다.

 

  JVM은 GC과정에서 메모리 단편화를 없애기 위해서 Compaction이라는 과정을 거친다. 즉, 현재 사용 중인 객체를 한 곳으로 모아서 파편화 현상(Memory Fragmentation)을 해결하는 것이다. 결국 Wrapper클래스를 사용해서 어떤 연산을 수행하면 힙영역에 메모리가 반복적으로 올라갔다 GC에 수거를 당하면서 Memory Fragmentation이 발생하고 이를 해결하기 위해 Compaction까지 진행해야한다. 성능이 느려지는게 당연한 현상인것이다. 

 

 

 

4. 정리

  • 어떤 이유(컬렉션에 넣는 등)가 필요 하지 않으면 기본형을 사용해야한다.
  • 오토박싱 언박싱이 코드상의 번거로움은 줄여주지만 위험까지 없애주지는 않는다.

댓글