자바 자료형에 대한 고찰 - 자바 Integer의 캐시기능
https://nulpointerexception.com/2018/01/06/int-vs-integer-java-application-memory-usage/
초반부 내용은 다음 원문을 번역해보았습니다.
int와 Integer 메모리 할당량의 차이 문제
int a = 100;
Integer b = new Integer(100);
저 두개의 선언을 각각 1번 2번이라 하자. 1번이 쓰는 메모리의 양 대비 2번이 쓰는 메모리의 양의 비율이 어떻게 될 것인가?
- 1
- 1.5
- 2
- 4
답은 뒤에서 설명한다.
32bit 컴퓨터를 쓴다는 것은 무엇인가?
기계, 서버, 컴퓨터 (문맥에서는 다 같은 용어로 칭한다)는 CPU 레지스터를 가지고 있다. 이런 레지스터들은 특정 사이즈를 가지고 있다. 32 bit 컴퓨터는 32비트의 CPU 레지스터 크기를 가진다. CPU 레지스터는 메모리의 주소를 저장한다. 그리고 그것은 프로세서에 의해 RAM의 데이터에 접근하게 해준다.
32 bit 컴퓨터는 2의 32승에 해당하는 메모리 위치에 접근하게 해주는데 다른 말로하면 00000000000000000000000000000000 부터 11111111111111111111111111111 의 메모리 영역을 의미하는데 총 4294967296개의 메모리 주소를 가질 수 있다. (이것은 32 bit 컴퓨터가 4GB의 메모리영역에 접근하고 각 메모리 주소는 1byte를 차지 한다는 가정하에 나온 계산이라고 한다)
이제 우리는 32bit 컴퓨터 (4GB의 램과함께) 에서 자바 애플리케이션을 실행한다. 4GB의 메모리 중 절반은 OS와 JVM이 쓰고 3GB정도가 남을 것이다.
Java Heap Memory는 클래스의 객체들을 위해 쓰이고 Native Heap Memory는 쓰레드 생성과 같은 OS와 관련된 일들을 담당한다. Native Heap Memory나 Java Heap Memory 둘 중 어느것이 부족해도 OutOfMemoryError가 발생한다.
Integer b = new Integer(100)를 하면 어떤 일이 일어날까?
앞서 제시한 문제에서 b 객체는 단지 100을 저장하는 것이 아니다. 그 대신에 관련된 정보를 저장한다.
- 클래스 포인터: 위의 경우에서는 Integer 클래스의 포인터이다.
- 플래그(Flag): 객체의 상태를 저장하는 플래그 값
- 락(Lock): 동기화(synchronization)를 지원하기 위한 값
- int 값인 100
클래스 포인터, 플래그, 락, int 값은 각각 32 bit를 차지한다. 그래서 총 128 bit를 차지하게 된다. 그래서 실제 저장하려는 값(단순 int 값)보다 대략 4배를 차지하게 되는 것이다.
그럼 int[] data = new int[1] 는 어떨까?
우리는 길이가 1인 배열을 가지고 있다. 이것은 메모리를 어떻게 차지할까
- 클래스 포인터: 우리의 경우에는 int[] 포인터
- 플래그(Flag): 객체의 상태를 저장하는 플래그 값
- 락(Lock): 동기화(synchronization)를 지원하기 위한 값
- 배열의 사이즈
- int 값인 100
이것도 마찬가지로 각각 32bit를 차지한다. 그래서 총 160bit를 쓴다. 저장하려는 값 대비 대략 5배를 차지하게 된다. 하나의 배열 엔트리는 Integer 객체보다 메모리 관점에서는 더 비싸다는 것을 알 수 있다.
그러면 String greet = new String("abcdefgh"); 의 메모리 사용량은 어떨까?
String은 두 개의 객체를 생성한다. 첫 번째는 아래와 같은 모습을 가졌다. value 필드에는 실제로 "abcdefgh"가 저장된 곳을 가리키는 주소값이 저장되어 있다.
첫 번째 객체는 32bit * 7의 사이즈로 224bit를 차지한다. 두 번째 객체는 (아래 사진) 256bit를 차지한다.
그래서 총 토탈 224 + 256 = 480 bit를 차지하게 된다. 이 말은 480 / 128 = 3.75의 메모리 오버로드가 생긴다.
primitive type
자바에서 원시 타입 (primitive type)에는 byte, short, int, long, float, double, char, boolean이 있다. 그리고 이 원시 타입은 null 값을 가질 수 없다. 원시 타입은 Stack 메모리 영역에 할당이 되게 된다. 그리고 실제 값을 가지게 된다.
예를들어, int 형을 보자. 자바는 int는 32 bit 메모리영역에 할당한다. 다른말로는 -(2의 31승) 부터 (2의 31승 -1) 까지의 값을 가질 수가 있게 된다. signed 값이므로 음수까지 표현하기 위해 2의 32승이 아니라 2의 31승까지이다.
reference type과의 비교
직접 코드를 통해서 확인해보자.
int a = 127;
int b = 127;
System.out.println(a == b);
true를 반환한다.
Integer aObject = 128;
Integer bObject = 128;
System.out.println(a == b);
false를 반환한다. 두 참조변수는 다른 객체를 가리키고 있기 때문이다.
그런데 눈여겨볼 만한 점은,
Integer aObject = 127;
Integer bObject = 127;
System.out.println(a == b);
이것의 결과는 true이다. 다른 객체를 가리키고 있을 것만 같던 이 두 참조 변수가 어떻게 같은 객체를 가리키고 있는 것일까?
그에 대한 해답은 밑에서 살펴보자.
Integer 클래스 해부해보기
Integer 클래스의 상단 부분이다. 최댓값과 최솟값을 상수형태로 가지고 있다.
문제의 valueOf를 찾아보자.
valueOf로 들어온 값이 저 분기문을 탄다면 (즉 IntegerCache에 들어있는 최댓값과 최솟값의 사이라면) 캐시된 값을 리턴해주고 그렇지 않다면 새로 객체를 생성해서 돌려주게 된다.
그렇다면 IntegerCache를 찾아보자. 찾아보니 Integer 클래스 내부의 private inner 클래스로 되어 있었다.
line 998 부터 (jdk 1.8 기준) inner 클래스에서 쓰이는 변수들이 정의되어있다. 일단 low 값은 고정이어서 변하지 않는다. 그냥 클래스 고정 스펙인듯 하다.
그리고 static 블럭안에 line 1005 부터 VM의 설정값을 읽어온다. 이것은 위의 초록색 주석에서도 보여지듯이 사용자가 캐시할 Integer들의 최댓값을 설정할 수가 있는데 그 설정값을 읽어오는 것이다.
line 1010: i와 127의 최댓값을 추출하는데 다시 말하면 127 미만의 값은 설정할 수 없다는 것을 의미한다.
line 1012: 음수 -128 부터의 값을 보장하기 위해 아무리 큰 값을 설정해도 음수를 위한 공간을 남겨놓는 것을 볼 수 있다.
line 1013: 설정값에 문제가 있을 때는 그냥 무시하는 모습. 이 에러를 무시해도 밑의 로직에는 영향이 없다.
line 1017: high 값을 변경된 h로 세팅하는 모습. (설정값이 없다면 그냥 127로 세팅)
line 1025: 캐시되는 최대값이 127 이상임을 보장한다.
line 1028: 밖에서 이 클래스의 객체를 생성할 필요가 없기 때문에 미연에 생성자를 private으로 해놓아 방지하는 모습.
이로써 위에서 각각 127로 초기화한 Integer 객체가 서로 같다고 나오는 이유를 알 수 있었다.
'프로그래밍 > Java' 카테고리의 다른 글
[Servlet] 웹서버 vs 애플리케이션 서버 vs 서블릿 컨테이너 - Tomcat 은 서블릿 컨테이너일까? (3) | 2019.10.09 |
---|---|
[Java] 자바의 String 클래스의 특별성 간단 정리 (0) | 2019.07.06 |
[JDBC] MySQL 드라이버 Class.forName()의 비밀 - 로드만 했을 뿐인데 getConnection()이 가능하다고? (6) | 2019.06.04 |
[Java] 추상클래스(Abstract Class)와 인터페이스(Interface)에 대한 고찰 (0) | 2018.11.28 |
[Java] 람다(Lamda)식과 함수형 인터페이스 (Functional Interface) (0) | 2018.11.19 |