목차
자바에서는 자료형을 2가지로 구분할 수 있다.
- 원시 자료형 (Primitive Type)
- 참조 자료형 (Reference Type)
원시 자료형 (Primitive Type)
보통의 int, boolean, float 같은 타입이다.
C나 자바 같은 전통적인 프로그래밍 언어들은 원시자료형을 제공한다.
특징
- 객체가 아님: 객체가 아니기 때문에 메모리에서의 위치를 통해 값을 직접 다루며 이 때문에 참조형에 비해 더 효율적이다.
- 자동 형변환/암시적 형변환: 원시 자료형끼리 자동 타입 변환이 지원된다. (int를 long에 할당할 수 있다. 하지만 반대로는 명시적인 형변환이 필요)
- 기본값: 원시 자료형은 기본 값을 가진다. int는0 boolean은 false, char는 null일 가진다
참조 자료형 (Reference Type)
원시형이 아닌 자료형 모두를 의미한다.
주요 참조 자료형
- 객체 (Object): String, Integer..
- 배열: 배열도 객체로 간주되므로 참조자료형이다.
- 인터페이스: 인터페이스도 참조 자료형으로 간주된다.
원시형에 대응되는 참조 자료형
특징
NULL이 될 수 있다.
메모리 주소를 가리킴
Collection은 참조형만 가능하다.
참조형이 데이터를 참조하는 방식
JVM에서 메모리 영역은 스택과 힙으로 나눌 수 있다.
- 스택: 메서드 호출 시 생성되는 지역변수와 참조형 변수의 메모리를 저장
- 힙: 객체와 배열이 생성되는 메모리 공간. 즉, 모든 객체는 힙에 저장된다. 객체의 생명은 명시적으로 힙에서 제거될 때 까지 유지된다.
그래서 객체를 아래처럼 생성하면 객체는 힙에 저장된다.
Dog dog = new Dog("Buddy");
이렇게 생성된 객체의 주소가 dog이라는 참조형 변수에 저장된다. 실제 데이터가 아닌 객체의 메모리 주소를 가리킨다.
그래서 다른 참조형 변수도 동일한 객체를 가리킬 수 있다.
Dog anotherDog = dog; // dog와 동일한 객체를 참조
anotherDog.bark(); // "Buddy says Woof!" 출력
anotherDog.name = "Max"; // 객체의 속성 변경
dog.bark(); // "Max says Woof!" 출력 (변경 사항 반영)
GC가 일어나면 사용하지 않는 객체를 자동으로 메모리에서 제거한다.
사용하지 않는 객체라면 참조형 변수가 null로 설정되거나 다른 객체를 참조하면 이전 객체는 더 이상 참조하지 않게되어 GC에 의해 회수 된다.
박싱과 언박싱
- 박싱: 원시 자료형을 참조 자료형으로 변환하는 과정. (int -> Integer)
- 언박싱: 참조 자료형을 원시 자료형으로 변환하는 과정 (Integer -> int)
박싱
AutoBoxing, 즉 원시 자료형이 자동으로 참조 자료형으로 변환될 수 있다. (Java5 부터 지원) 이 때 박싱과정에서 객체를 생성하기 때문에 메모리 오버헤드가 발생할 수 있다.
int primitiveInt = 5;
Integer boxedInt = primitiveInt; // 자동 박싱
언박싱
Unboxing은 참조형이 원시형으로 자동으로 변환되는 것을 의미한다. (Java5 부터 지원) 이 때 참조형이 null이면 언박싱하다가 NPE가 발생할 수 있다.
Integer boxedInt = null;
int primitiveInt = boxedInt; // NullPointerException 발생
박싱 과정에서 결국은 객체를 생성하여 힙에 할당하고 (그 객체가 차지하는 리소스도 더 큼) 힙에 할당한 객체들은 사용하지 않을 경우 GC의 대상이 되기 때문이다.
정리하면 반복적인 박싱/언박싱은 성능 저하를 일으킬 수 있어서 주의할 필요가 있다.
원시형과 참조형의 속도 비교
동일한 동작을 원시형과 참조형을 비교해서 속도비교를 해본다.
1억 개의 element를 원시형 int[]에 삽입하고 다시 그 값을 찾는 속도를 측정해본다.
// int[]에 1억개 삽입
long start = System.currentTimeMillis();
int[] intElements = new int[100000000];
for (int i = 0; i < 100000000 - 1; i++) {
intElements[i] = 1;
}
intElements[100000000 - 1] = 2;
// int[] 1억 개 중 찾기
int idx = 0;
while (2 != intElements[idx]) {
idx++;
}
long end = System.currentTimeMillis();
System.out.println(end - start + "ms");
결과: 228ms
long start = System.currentTimeMillis();
Integer[] intElements = new Integer[100000000];
for (int i = 0; i < 100000000 - 1; i++) {
intElements[i] = 1;
}
intElements[100000000 - 1] = 2;
// int[] 1억 개 중 찾기
int idx = 0;
while (2 != intElements[idx]) {
idx++;
}
long end = System.currentTimeMillis();
System.out.println(end - start + "ms");
결과: 707ms
결과적으로 3배 이상의 차이가 발생한다. 하지만 참조형의 경우 편리한 기능을 많이 제공하기 때문에 필요하다면 사용하는게 맞고 무분별하게 참조형을 사용하지 않도록 한다.
메모리 사용 측면에서도 int (원시형) 은 32비트 즉 4바이트를 사용하며, Integer(참조형)은 128비트 즉 16바이트를 사용한다.
참조형은 값이 보관되는 영역은 4바이트로 동일하지만 나머지 12바이트는 여러 부가정보를 보관하는 헤더로 구성된다고 한다.
오토 박싱과 언박싱을 피하는 방법
- 기본형 타입을 많이 사용하기
- 컬렉션을 사용할 때는 IntStream 같은 스트림API 사용을 고려해본다. (스트림API는 박싱/언박싱을 회피하려는 설계를 가지고 있다고 한다)
// 객체 스트림을 사용할 경우
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int sum = list.stream().mapToInt(Integer::intValue).sum(); // 언박싱이 발생
// 기본형 스트림을 사용할 경우
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
int sum = intStream.sum(); // 박싱/언박싱 없이 처리
원시 타입, 참조 타입 어떤 것을 가지고 싶은지에 따라 parseXX, valueOf 구분하기
int 타입을 예로 들면 parseInt, valueOf 둘 다 int 타입으로 받을 수 있다.
하지만 결국 내부적으로 valueOf를 통해 int 타입으로 변환한다면 내부적으로 오토 언박싱이 발생한다고 한다.
=> int 타입으로 받으면 결국 둘 다 내부적으로 오토 언박싱이 발생함.
하지만 valueOf의 경우 Integer 타입으로 받는다면 캐싱을 기대할 수 있다. 캐싱 범위 안이라면 새로운 객체를 만들지 않고 캐싱된 결과를 돌려주어 성능 향상을 좀 더 기대할 수 있다고 한다.
참조
https://f-lab.kr/insight/java-auto-boxing-and-unboxing
'Java' 카테고리의 다른 글
컬렉션 프레임워크 (2) | 2024.10.05 |
---|---|
Java가 빌드되고 실행되는 과정 (3) | 2024.10.03 |
함수형 인터페이스 (Functional Interface) 에 대해 (2) | 2024.10.03 |
제네릭 (Generic) 에 대해 (1) | 2024.10.03 |
Java의 주요 특징 (1) | 2024.10.03 |