from java code to java heap - portfolio |...

31
From Java code to Java heap Java 어플리케이션의 메모리 사용 방식을 이해하고 최적화하는 방법 저자: Chris Bailey ([email protected]), Java Service Architect, IBM 작성일: 2012 년 2 월 29 일 대상: 중급 개발자 번역: 김형석 수석 번역일: 2013 년 02 년 12 일 원문 주소: http://www.ibm.com/developerworks/java/library/j-codetoheap/index.html 어플리케이션의 메모리 사용 효율을 높이는 문제가 그리 새로운 것이 아님에도 불구하고, 이 내용을 제대로 이해하고 있는 사람이 그리 많지 않다. 이 문서는 Java 프로세스가 메 모리를 사용하는 방식으로부터 시작해서, 개발자가 작성한 Java 코드가 메모리를 사용하 는 방식까지를 상세히 다룰 것이다. 마지막으로, 코드를 작성할 때 – 특히 HashMap과 ArrayList를 사용하는 부분에서- 보다 효율적으로 메모리를 사용할 수 있도록 하는 방법 에 대해 언급할 것이다.

Upload: others

Post on 05-Nov-2019

3 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

From Java code to Java heap Java 어플리케이션의 메모리 사용 방식을 이해하고 최적화하는 방법

저자: Chris Bailey ([email protected]), Java Service Architect, IBM

작성일: 2012년 2월 29일

대상: 중급 개발자

번역: 김형석 수석

번역일: 2013년 02년 12일

원문 주소: http://www.ibm.com/developerworks/java/library/j-codetoheap/index.html

어플리케이션의 메모리 사용 효율을 높이는 문제가 그리 새로운 것이 아님에도 불구하고,

이 내용을 제대로 이해하고 있는 사람이 그리 많지 않다. 이 문서는 Java 프로세스가 메

모리를 사용하는 방식으로부터 시작해서, 개발자가 작성한 Java 코드가 메모리를 사용하

는 방식까지를 상세히 다룰 것이다. 마지막으로, 코드를 작성할 때 – 특히 HashMap과

ArrayList를 사용하는 부분에서- 보다 효율적으로 메모리를 사용할 수 있도록 하는 방법

에 대해 언급할 것이다.

Page 2: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

배경지식: Java 프로세스의 메모리 사용 방법

명령실행줄(Command Line)에서 java 명령어를 이용하여 Java 어플리케이션이나 (WAS등

과 같은) Java 기반 미들웨어를 실행하면 Java 런타임은 새로운 OS 프로세스를 하나 생

성한다. 그리고 이는 OS 프로세스라는 부분에서 C 기반 프로그램과 완벽하게 동일하다.

사실, JVM의 상당 부분은 C 혹은 C++로 작성되어 있으니 당연한 일이기도 하다. 하나의

OS 프로세스로서, Java 런타임에는 모든 다른 프로세스들과 동일한 메모리 사용 제한이

있다. 아키텍처에 의해 제공되는 메모리 주소 크기(Addressability)1와 OS에 의해 제공되

는 사용자 영역(User Space)이 그것이다.

아키텍처가 제공하는 메모리 주소 크기는 프로세서의 비트(bit) 크기에 의해 결정된다. 프

로세서의 비트 크기는 32bit, 64bit 등과 같은 크기를 말하며 IBM 메인프레임의 경우에는

31bit 크기를 사용하기도 한다. 프로세서가 처리할 수 있는 비트의 크기는 프로세서가 할

당할 수 있는 메모리의 크기를 결정한다. 32bit 프로세서는 2의 32제곱 크기(2^32), 즉

4,294,967,296bit(혹은 4gigabytes)의 메모리를 할당할 수 있고, 64bit 프로세서는 2의 64

제곱 크기(2^64), 즉 실로 엄청난 크기인, 18,446,744,073,709,551,616 bit(혹은

16exabytes2)를 할당할 수 있다.

프로세서 아키텍처에 의해 제공되는 메모리의 영역 중 일부는 OS의 kernel과 C 런타임

을 위해 사용된다. (JVM이 C와 C++로 작성되었으므로 C 런타임을 위한 영역이 필요하

다.) OS와 C 런타임이 사용하는 메모리의 크기는 어떤 OS를 사용하느냐에 따라 결정되

는데, 윈도우의 경우는 기본적으로 2GB를 사용한다.3 이를 제외한, 사용자 영역이라 불리

는, 나머지 할당 가능한 영역이 실제 프로세스가 실행되기 위해 사용할 수 있는 메모리

1 Addressability는 사전적으로 “주소 지정 가능도”라고 하며, ‘장치 공간에서 할당할 수 있는 주소

들의 수’라고 이해하면 된다.

2 1 Exabyte는 1018byte이다. 즉, 264bit = 16 * 1018B = 16 * 109GB = 16 * 106TB = 16 * 103PB =

16EB 이다. Exabyte와 관련된 대한 상세한 내용은 http://en.wikipedia.org/wiki/Exabyte를 참고하라.

참고로, 1KB는 1000B이기도 하고 1024B이기도 하다. 이 값은 사용되는 ‘맥락’에 따라 결정된다.

3 윈도우 OS가 사용하는 메모리에 대한 상세한 내용을 알고 싶으면

http://blogs.technet.com/b/sankim/archive/2009/05/21/4gb-32-windows.aspx을 참고하라.

Page 3: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

공간이 된다.

그러므로, Java 어플리케이션의 경우, 사용자 영역이 Java 프로세스에 의해 사용될 수 있

는 메모리가 되는데, 이 영역은 또다시 두 가지 영역, 즉 Java 힙(heap 4 )과 native(non-

Java) 힙으로 구분된다. JVM이 관리할 수 있는 Java 힙 메모리의 크기는 Java 프로세스를

구동할 때 설정하는 –Xms 값과 –Xmx 값에 의해 결정된다5. 그림 1은 32bit Java 프로세

스가 실행될 때의 메모리 배치를 개략적으로 나타낸 것이다.

그림 1. 32bit Java 프로세스를 위한 메모리 배치 예

위 그림에서 알 수 있듯이, OS와 C 런타임이 4GB중 1GB 가량을, Java 힙이 약 2GB, 그

리고 Native 힙이 나머지를 사용하고 있다. 위 그림을 보면 JVM도 일정 부분의 메모리를

사용하고 있는데, OS 커널이나 C 런타임과 마찬가지로 JVM도 실행되기 위해 메모리가

필요하기 때문이다. (그리고 JVM이 사용하는 메모리는 Native 힙의 일부이다.)

4 Heap은 원래 트리 기반 자료구조 중 하나이다. Java가 메모리 구조로 Heap을 이용한 이유에 대

해서는 다음을 참고하길 바란다. http://stackoverflow.com/questions/2787611/why-does-java-uses-

heap-for-memory-allocation

5 -Xms는 힙 메모리 최소값이고 –Xmx는 힙 메모리 최대값이다. -Xmx보다 더 많은 메모리를 할당

해야 하는 경우 OutOfMemoryError가 발생한다. 물론 Java에서 OutOfMemoryError가 발생하는 이

유는 이 뿐만이 아니다. Memory Leak이 발생하여 GC를 수행하여도 더 이상 메모리를 할당 할 수

없을 때, Thread를 더 이상 생성할 수 없을 때, File을 더 이상 생성할 수 없을 때 등등 매우 다양

한 상황에서 OutOfMemoryError가 발생할 수 있다.

Page 4: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

Java 객체 분석6

new 연산자를 이용하여 Java 클래스의 인스턴스를 만들 때, 실제로는 우리가 생각했던

것보다 훨씬 많은 일들이 일어난다. 예를 들어, Java에서 가장 작은 Object 중 하나인

Integer 클래스에 int값을 입력하기 위해 할당되는 추가적인 메모리의 양을 알게 된다면

깜짝 놀랄 것이다. 바로 얘기하면, 추가적인 메모리의 양은 원래 int 값의 네 배에 해당하

는 16byte가 필요하다.7 이 추가적인 메모리 오버헤드는 JVM이 Integer라는 클래스의 내

용을 기술하는 메타데이터를8 저장하기 위해 사용된다. 객체의 메타데이터 정보는 JVM의

버전이나 벤더에 따라 약간씩 다르지만, 대체로 다음 세 가지 정보를 포함하고 있다.

Class: 클래스에 대한 정보를 나타내는 포인터로, 객체의 유형이 적혀있다. 이 경우

java.lang.Integer 클래스를 가리킨다.

Flags: 객체의 상태를 서술하는 정보의 모음. 객체의 hashcode 값이 있다면 이곳에

저장된다. 또, 이 객체가 배열인지 아닌지에 대한 정보도 포함한다.

Lock: 객체에 대한 동기화 정보. 현재 이 객체가 동기화되고 있는지에 대한 정보를

나타낸다.

그리고 객체의 메타데이터 뒤로 객체 인스턴스에 포함되어 있는 필드와 같은 실제 객체

의 데이터가 추가된다. 이 java.lang.Integer 클래스의 예에서는 int형 데이터 하나가 메

타데이터 뒤에 추가될 것이다. 즉, 32bit JVM에서 Integer 객체 인스턴스를 생성한다면

다음 그림과 같은 방식으로 메모리를 사용하게 될 것이다.

6 원문에서는 ‘Object’라는 단어를 사용하고 있는데, 이 상황에서는 ‘인스턴스’라는 의미로 받아들

이는 것이 옳을 수 있다. Java 언어에서는 Object라는 단어의 의미가 여러 가지로 사용되곤 하는

데, 생성되어 메모리에 적재된 객체는 인스턴스라는 용어로 사용하는 것이 바람직할 것이다.

7 이미 알고 있겠지만 Java에서 원시자료형인 int의 크기는 4byte이다.

8 메타데이터(Metadata)는 어떤 데이터를 설명하기 위한 데이터를 의미한다. 메타데이터의 예로는,

사진 이미지 파일의 exif 정보, 웹페이지의 meta-tag들, 출판된 도서의 ISBN 등을 들 수 있다.

Page 5: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

그림 2. 32bit Java 프로세스에서의 java.lang.Integer 객체에 대한 메모리 배치 예

그림2에서 볼 수 있듯이, Integer 클래스의 메타데이터 때문에, 32bit의 int 데이터를 저장

하기 위해 총 128bit의 데이터가 필요하다는 것을 알 수 있다.

Java 객체 배열 분석

어떤 데이터(예를 들어 int 값)에 대한 배열의 메모리 구조는 일반적인 Java 객체와 거

의 비슷하다. 주요한 차이점은 배열의 크기를 나타내는 메타데이터가 추가된다는 점이다.

그러므로 객체 배열의 메모리 구조는 다음 요소들로 구성된다.

Class: 클래스에 대한 정보를 나타내는 포인터로, 객체의 유형을 서술한다. 이

경우에는 int형의 배열인데, 즉 int[] 클래스를 가리킨다.

Flags: 객체의 상태를 서술하는 정보의 모음. 객체의 hashcode 값이 있다면 이곳에

저장된다. 또, 이 객체가 배열인지 아닌지에 대한 정보도 포함한다.

Lock: 객체에 대한 동기화 정보. 현재 이 객체가 동기화되고 있는지에 대한 정보를

나타낸다.

Size: 배열의 크기

그림 3에서 int형 배열의 메모리 구조를 확인할 수 있다.

그림 3. 32bit 프로세서에서의 int 형 배열에 대한 메모리 배치 예

위 그림에서 볼 수 있듯이, 32bit의 int 자료를 위해 총 160bit의 자료를 저장해야 하는

것을 알 수 있다. 그래서, 하나의 데이터만 저장하는 경우라면 배열을 이용할 경우가 원

시자료형의 래퍼 클래스(Byte, Integer, Long과 같은)들을 이용하는 것보다 더 많은 메모

리를 필요로 하게 된다.

Page 6: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

좀 더 복잡한 객체의 메모리 구조 분석

제대로 된 객체 지향 설계와 객체 지향 프로그래밍은 캡슐화(데이터를 숨기고 그것을

제어하기 위한 인터페이스를 제공)와 위임(실제 작업 수행을 담당하는 도우미 클래스를

제공)을 적절히 잘 이용해야 한다고들 한다. 그런데 본질적으로, 캡슐화와 위임을 이용한

다는 것은 대부분의 경우 여러 가지의 클래스들이 서로 연관되어 있다는 것을 의미한다.

쉬운 예로 java.lang.String 클래스를 살펴보자. String 클래스 내에 캡슐화되어 String 클

래스에 의해 관리되는 데이터는 char 배열이다. 32bit 프로세서에서 String 객체는 다음과

같이 표현된다.

Figure 4. 32bit 프로세서에서의 String 클래스에 대한 메모리 배치 예

위 그림에서 보이듯, java.lang.String 클래스에는 기본적인 객체 메타데이터 외에도 네

가지 필드들이 포함되어 있는데, hash값(hash), 문자열의 길이(count), 문자열 데이터의 시

작점(offset), 그리고 실제 문자열 배열에 대한 참조(value)이다.

이 말은 여덟 개의 문자열(128bit의 char 데이터9)를 저장하기 위한 String 클래스를 위해

총 224bit의 메모리가 할당되고 String 클래스가 포함하고 있는 문자열 배열까지 합치면

총 480bit가 할당되어야 한다는 말이다. 즉, 128bit를 위해 480bit가 필요하다는 얘기이고,

정리하면 String 클래스의 오버헤드 비율은 3.75:1이 된다. 일반적으로 데이터 구조가 복

잡하면 복잡할수록 오버헤드도 더 커진다. 이 부분에 대해서는 뒤에 좀 더 자세히 다룰

것이다.

9 C와 달리 Java에서는 기본적으로 1char = 2byte = 16bit이다. 그래서 8char = 16*8bit = 128bit임

을 알 수 있다. 만약 java를 실행할 때, -XX:+UseCompressedStrings 옵션을 이용한다면, (문자열

저장이 ASCII 코드에 제한되긴 하지만) 1byte = 1char로 사용할 수도 있다. 이에 대한 자세한 내

용은 http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html을 참조하기

바란다.

Page 7: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

32-bit 와 64-bit Java 객체들

지금까지는 32bit 프로세스에 적용되는 메모리 크기, 배치, 오버헤드에 대해 다뤘다. 이

미 [배경지식: Java 프로세스의 메모리 사용 방법] 장에서 언급한 바 있지만, 64bit 프로세

서는 32bit 프로세서에 비해 사용할 수 있는 메모리 주소의 수가 엄청나게 크다. Java가

64bit 프로세스로서 실행되면, 몇몇 데이터 필드들, 특히 객체 메타데이터와 같은 정보를

위한 필드의 크기는 64bit에 맞게 증가되어야 한다. 물론 int나 byte, long과 같은 일반적

인 데이터 유형들의 크기는 변경되지 않는다. 그림 5를 보면 64bit에서 Integer객체와

int 배열의 메모리 배치를 확인할 수 있다.

Figure 5. 64bit 프로세서에서의 Integer 클래스와 int 배열에 대한 메모리 배치 예

위 그림은 64bit 프로세스에서 Integer 객체가 32bit int 데이터를 저장하기 위해 224bit

를 할당해야 하는 것을 보여준다. 그리고 이 오버헤드 비율은 7:1에 이른다. 또, 구성 요

소가 하나밖에 없는 int형 배열은 288bit를 할당해야 하는데, 역시 이 오버헤드 비율도

9:1로 32bit보다 훨씬 크다는 것을 알 수 있다. 실제로 예전에 32bit에서 실행되던 java

어플리케이션이 64bit에서 실행되면 훨씬 많은 양의 메모리를 사용하게 되는 것을 볼 수

있다. 일반적으로 64bit로 실행될 때 약 70%가량 heap 메모리를 더 사용하는 것으로 간

주된다. 즉, 32bit Java 런타임에서 1GB의 메모리를 사용하는 Java 어플리케이션이라면

64bit Java 런타임에서는 일반적으로 1.7GB를 사용하게 된다는 말이다. 물론 이런 메모리

사용량 증가가 Java 힙 메모리에 국한된 것은 아니다. 위에서 설명했던 Native 힙 메모리

의 사용량도 증가하게 되는데, 증가된 비율이 90%에 이를 때도 있다.

아래 표1을 보면 32bit와 64bit로 실행될 때 객체와 배열의 필드들을 위해 할당되는 메

모리의 양을 알 수 있다.

Page 8: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

Field type Field size (bits)

Object Array

32-bit 64-bit 32-bit 64-bit

boolean 32 32 8 8

byte 32 32 8 8

char 32 32 16 16

short 32 32 16 16

int 32 32 32 32

float 32 32 32 32

long 64 64 64 64

double 64 64 64 64

Object fields 32 64 (32*) 32 64 (32*)

Object metadata 32 64 (32*) 32 64 (32*)

표 1. 32Bit 와 64bit Java 런타임 내에서 필드들의 메모리 크기

*객체 필드와 객체 메타데이터의 크기는 Compressed References 기술 혹은 Compressed

OOPs 기술을 통해 32bit를 사용하도록 설정할 수 있다.

Compressed References and Compressed Ordinary Object Pointers (OOPs)

IBM과 Oracle의 JVM들은 모두 Compressed References (-Xcompressedrefs) 기술과

Page 9: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

Compressed OOPs (-XX:+UseCompressedOops) 기술을 제공10하는데, 이 기법을 이용하

면 객체를 64bit 방식으로 저장하지 않고 32bit 방식으로 저장할 수 있다. 이 방법을 이

용할 경우, 위에서 말한 70%의 메모리 추가사용을 막을 수 있다. 하지만 이 옵션은

Native 힙 메모리의 사용량에는 영향을 주지 않기 때문에, Native 힙에 대해서는 여전히

64bit 프로세스가 32bit 프로세스에 비해 더 많은 메모리를 사용하게 된다.

10 참조 압축과 객체 압축 기법. 하지만 이 옵션을 지정할 경우 예기치 않게 JVM이 정지(crash)하

는 일이 발생하기도 한다.

Page 10: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

Java 컬렉션 클래스들의 메모리 사용 방식

대부분의 어플리케이션들이 대량의 데이터를 저장하고 관리하기 위해 JavaSDK가 제공

하는 표준 컬렉션(Collection) 클래스를 이용한다. 만약 메모리 효율성이 중요한 어플리케

이션을 만들어야 한다면, 각 컬렉션 구현체들의 세부적인 기능을 이해하고, 각각의 메모

리 오버헤드를 알고 있어야 한다. 일반적으로, 컬렉션 구현체의 기능이 많을수록 메모리

오버헤드는 증가한다고 생각하면 된다. 그러므로, 실제로 필요하지 않은데도 과도하게 기

능이 많은 컬렉션 구현체를 사용하는 것은 그만큼 쓸데없이 메모리를 낭비하는 것이 된

다.

자주 사용되는 컬렉션 클래스들은 다음과 같다.

- HashSet - HashMap - HashTable - LinkedList - ArrayList

HashSet를 제외하면, 이 목록은 기능과 오버헤드가 큰 순서부터 적은 것이다. (HashSet

은 HashMap의 래퍼 클래스로, HashMap의 기능을 다소 제한했지만 메모리 사용량은 약

간 더 크다.)

Java Collections: HashSet

HashSet은 Set 인터페이스의 구현체이다. JavaSE6 API 문서를 보면 HashSet이 다음과

같이 설명되어 있다.

HashSet 내에는 동일한 요소(element)가 존재하지 않는다. 일반화해서 말하면, 객체

e1 과 e2 에 대해 e1.equals(e2)가 true 인 e1 과 e2 가 존재하지 않으며, null 요소도

최대 한 개만 저장된다. 이 클래스의 이름에서 알 수 있듯이, 이 인터페이스는

수학에서 말하는 ‘집합’을 추상화한 것이다.

HashSet은 HashMap에 비해 제한적인데11, 위에 언급되어 있듯이 중복되는 요소가 하나

도 없고 null 요소도 많아야 하나만 입력될 수 있다. 실제 구현은 HashMap의 래퍼 클래

스인데, 자신에게 입력된 값을 HashMap에 저장하고 이를 관리하는 기능이 구현되어 있

다고 생각하면 된다. 요소들의 중복을 제한하는 추가적인 기능 때문에 HashSet이

11 기능이 제한적이라는 말로 오해할 수도 있지만, 저장될 수 있는 데이터의 조건이 제한적이라고

이해하는 것이 더 옳을 듯 하다. 그래서 실제로 기능적으로만 본다면 “데이터를 제한하는 기능”이

추가되어 있다고 생각하면 될 것이다.

Page 11: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

HashMap에 비해 약간 더 메모리 오버헤드가 크게 된 것이다.

그림 6은 32bit Java 런타임에서 HashSet의 메모리 사용 방식을 보여주고 있다.

그림 6. 32bit Java 프로세스에서의 HashSet 객체에 대한 메모리 배치 예

그림 6은 java.util.HashSet 객체가 메모리에 적재되었을 때, 실제 HashSet객체가 사용하

는 메모리(Shallow 힙)와 HashSet 내의 데이터를 포함했을 때의 메모리(Retained 힙) 사

용량을 보여준다. HashSet에 대해 Shallow 힙 메모리는 16byte이고 Retained 힙 메모리

가 144byte인 것을 알 수 있다. HashSet 객체가 처음 생성되면, 최초 할당되는 기본 용

량은 16이며, 이 말은 HashSet이 최초 생성되었을 때, 16개의 요소를 담을 수 있도록 되

어 있고 아직은 아무 요소도 입력되지 않았지만 기본적으로 144byte를 사용한다는 것이

다. 그리고 이 크기는 HashMap의 메모리 사용량(128byte)보다 16byte가 큰 양이다. 표2

에 HashSet의 상세한 속성을 적어놓았다.

기본 용량 16개

텅 비었을 때의 메모리 크기 144 bytes

오버헤드 HashMap 오버헤드 + 16byte

1 만개의 요소에 대한 오버헤드 HashMap 오버헤드 + 16byte

검색/추가/삭제 성능 O(1)12 — 해시 충돌13이 없다면 요소의 수가 작업 속도에 영향을 미치지 않는다

표 2. HashSet 객체의 특징

12 이와 같은 표기법을 Big O notation, 빅 오 표기법이라고 부르는데, 특정 알고리즘의 성능을 수

학적으로 표기하기 위해 사용된다. 대표적인 예로, O(1), O(n), O(nc), O(2n), O(logn) 등이 있다. n은

요소의 개수를 나타내며, 여기서 말한 O(1)은, 성능이 요소의 개수에 영향을 받지 않는 것을 의미

한다. Big O notation에 대해서는 http://en.wikipedia.org/wiki/O_notation를 참조하라.

13 Hash Collision. Hash 키 방식을 이용할 경우 해시 충돌이 일어날 가능성이 있다. 입력 값의 개

수를 미리 알고 있고, 그 개수가 적은 경우에는 해치 충돌을 피하도록 할 수 있지만, 그렇지 않은

경우에는 언제나 해시 충돌이 발생할 수 있다. 자세한 내용은

http://en.wikipedia.org/wiki/Hash_function를 참조하라.

Page 12: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

Java Collections: HashMap

HashMap은 Map 인터페이스의 구현체이다. HashMap은 JavaSE6 API 문서에 다음과 같

이 설명되어 있다.

HashMap 은 키(key)와 값(value)를 연결한 객체이다. 이 객체에는 하나 이상의

동일한 키가 저장될 수 없으며, 각각의 키는 많아야 단 하나의 값 14을 가리킬 수

있다.

HashMap을 이용하면 키/값을 쌍으로 저장할 수 있는데, 키를 해싱(hashing)하여 특정

인덱스로 만든 뒤 그 값을 이용하여 키와 값을 저장한다. 이렇게 하면 저장된 키를 고속

으로 검색할 수 있다. Null 혹은 중복값 입력도 허용되는데, 이런 점에서 볼 때,

HashMap은 HashSet의 단순화 버전이라고 생각할 수 있다.

HashMap은 HashMap$Entry 객체의 배열을 구현해 놓은 것이다. 그림 7에서 32bit Java

런타임에서 HashMap 객체가 생성되었을 때의 메모리 사용 방식을 볼 수 있다.

그림 7. 32bit Java 프로세스에서의 HashMap 객체에 대한 메모리 배치 예

그림 7을 보면, HashMap 객체가 최초 생성되면 HashMap 객체와 더불어

HashMap$Entry 객체 배열이 생성되고 배열의 기본 크기가 16개임을 알 수 있다. 그리

고 텅 빈 HashMap 객체가 128byte의 메모리를 소비하는 것을 알 수 있다. 키/값 쌍이

입력되면 HashMap$Entry 객체에 의해 캡슐화되어 저장되는데, 이 객체에도 일정한 수

준의 오버헤드가 존재한다.

HashMap$Entry 객체가 포함하고 있는 필드들은 다음과 같다.

int KeyHash

Object next

Object key

Object value

32bit 프로세스15에서 HashMap$Entry 객체는 컬렉션 속에 입력될 키/값 쌍을 관리한다.

14 특정 값 혹은 null

Page 13: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

즉, HashMap 객체의 오버헤드는 HashMap 자체의 오버헤드와 각각의 HashMap$Entry

객체의 오버헤드를 더한 값이라고 생각하면 된다. 그래서 이런 식이 만들어 질 수 있다.

HashMap object

+ Array object overhead

+ (number of entries * (HashMap$Entry array entry + HashMap$Entry object))

1만 개의 요소가 입력되어 있는 HashMap을 생각해보면, 오버헤드만 약 360KB에 이른다.

당연히 이 값은 키와 값을 위한 메모리 소비량은 포함하지 않은 값이다.

표3에서 HashMap에 대한 상세한 내용이 설명되어 있다.

기본 용량 16 개

텅 비었을 때의 메모리 크기 128 bytes

오버헤드 64 bytes + 입력 요소마다 36 bytes씩 증가

1 만개의 요소에 대한 오버헤드 ~360K

검색/추가/삭제 성능 O(1) — 해시 충돌이 없다면 요소의 수가 작업 속도에 영향을 미치지 않는다.

표 3. HashMap 객체의 특징

Java Collections: Hashtable

Hashtable은 HashMap과 마찬가지로 Map인터페이스 구현체이다. JavaSE6 API 문서에서

설명된 Hashtable은 다음과 같다.

Hashtable 은 키와 값을 연결해주는 구현체이다. Null 이 아닌 모든 객체는 키나

값으로 사용될 수 있다.

Hashtable은 HashMap과 상당히 유사하지만, 두 가지 제한사항이 존재한다. 키/값 모두

에 null 값을 입력할 수 없다는 것과, Hashtable이 동기화된 클래스 16 라는 것이다.

15 원문에는 32bit HashMap$Entry object라고 표기되어 있어 그대로 적긴 했지만, 굳이 32bit라는

말이 들어갈 필요는 없을 것으로 보인다. 32bit 프로세스에서 오버헤드의 크기가 36byte라는 것을

나타내기 위해 굳이 32bit라는 말을 쓴 것으로 보인다.

16 다른 말로 이 클래스의 객체가 Thread-Safe하다고 말할 수 있다. Thread-Safe 객체라는 개념은

기본적으로는 멀티스레드 환경에서 정상적으로 개발자가 의도한 대로 동작하는 객체를 의미하지

만 이 말 만으로는 Thread-Safe의 모든 상세 내용을 설명하기 어렵다. Thread-Safe에 대한 상세한

내용을 알고 싶다면 Addison Wesley 에서 출판한 [Java Concurrency in Practice]를 참고하기 바란

Page 14: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

HashMap은 반대로 null 키/값을 입력할 수 있으며 동기화되어 있지도 않다. 만약 동기

화된 HashMap을 만들고 싶으면 Collections.synchronizezdMap() 메소드를 이용해야 한

다.

Hashtable도 HashMap과 동일하게 Hashtable$Entry라는 객체의 배열을 관리하도록 구

현되어 있다. 그림8에서 32bit Java 런타임에서의 Hashtable의 메모리 사용 방식을 확인

할 수 있다.

그림 8. 32bit Java 프로세스에서의 Hashtable 객체에 대한 메모리 배치 예

그림8에서 볼 수 있듯이, Hashtable이 최초 생성되면 순수하게 Hashtable 객체를 위해

서 40byte가 할당되는 것을 알 수 있다. 나머지 64byte는 Hashtable$Entry 객체 배열을

생성하는 데 사용되는데, HashMap과는 다르게 기본 용량은 11인 것을 알 수 있다. 어쨌

든 최초 생성한 Hashtable을 위한 초기 메모리 할당 크기는 104byte가 된다.

Hashtable$Entry 클래스는 HashMap$Entry 클래스와 동일한 형태이다.

int KeyHash

Object next

Object key

Object value

이 얘기는 Hashtable$Entry 객체 하나마다 32byte의 오버헤드가 발생한다는 것을 의미

하고, 1만 개의 키/값이 입력되면 HashMap과 같이 약 360KB의 오버헤드가 발생하게 된

다.

표 4에서 Hashtable의 상세한 속성을 확인할 수 있다.

기본 용량 11 entries

텅 비었을 때의 메모리 크기 104 bytes

오버헤드 56 bytes + 요소마다 36 bytes 추가

다.

Page 15: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

1 만개의 요소에 대한 오버헤드 ~360K

검색/추가/삭제 성능 O(1) — 해시 충돌이 없다면 요소의 수가 작업 속도에 영향을 미치지 않는다

표 4. Hashtable 객체의 특징

이미 눈치챘겠지만, Hashtable의 기본 용량은 HashMap의 기본 용량보다 약간 작다.(11

개 vs 16개) 그것 말고는 null에 대한 입력 불가나 동기화와 같은 기능적인 차이점 밖에

없는데, 이런 부분들은 메모리 사용량이나 컬렉션의 성능을 향상시키는 데에는 꼭 필요

한 것은 아닐 수 있다.17

Java Collections: LinkedList

LinkedList는 List 인터페이스에 대한 연결 리스트 형태의 구현체이다.18 JavaSE6 API 문

서에서 말하는 LinkedList는 다음과 같다.

LinkedList는 순서가 정해져 있는 컬렉션 클래스이다. (시퀀스라고도 불린다.) 이 인터페

이스를 이용할 경우, 리스트 내에 하나 하나의 요소들이 입력되는 위치에 대한 세밀한

제어가 가능해진다. 정해진 정수형의 인덱스를 이용하여 리스트 내의 특정 요소에 접근

하거나 검색할 수 있다. Set 인터페이스와는 달리 동일한 요소가 입력될 수 있다.

LinkedList는 LinkedList$Entry 객체 목록을 구현한 것이다. 그림 9를 보면 32bit Java 런

타임에서 LinkedList를 사용할 때의 메모리 사용 방식을 알 수 있다.

그림 9. 32bit Java 프로세스에서의 LinkedList 객체에 대한 메모리 배치 예

17 Hashtable은 동기화 처리가 필요하지 않은 상황에서도 동기화 처리가 수행되도록 구현되어 있

다. 그리고 이 부분에서 HashMap에 비해 성능이 떨어질 ‘수’도 있다. 만약 동기화된 Map 구현체

를 사용하고 싶다면 Hashtable이나 Collections.synchronizezdMap()을 이용하기 보다는, JavaSE5

부터 지원되는 ConcurrentHashMap을 이용하기를 권장한다.

18 정말 동어반복적인 설명이다.

Page 16: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

그림9에서 볼 수 있듯이, LinkedList가 최초 생성되면 LinkedList객체 자체를 위해

24byte, 그리고 하나의 LinkedList$Entry 객체를 위해 다시 24byte를, 그래서 총 48byte

의 메모리가 할당된다. LinkedList 클래스의 장점들 중 하나는, 항상 리스트의 크기가 정

확하고, 크기 변경이 일어날 필요가 없다는 것이다. LinkedList의 기본 용량은 1인데, 새

로운 요소가 추가되거나 기존의 요소가 제거될 때마다 즉각적으로 이 값이 변경된다. 그

렇다고 하더라도 LinkedList$Entry 객체에도 오버헤드가 있는데, 이 클래스 내에 포함되

어 있는 필드들은 다음과 같다.

Object previous

Object next

Object value

물론 이 크기는 HashMap이나 Hashtable의 오버헤드에 비하면 작다고 할 수 있는데, 그

이유는 LinkedList가 단 하나의 요소만 포함하고 있고 키/값 쌍을 저장할 필요도 없고,

추후 검색을 위해 키를 해싱하여 저장할 필요도 없기 때문이다. 반면 부정적인 측면도

있는데, LinkedList내에서 검색을 수행하는 것은 훨씬 느린데, 특정 요소를 찾기 위해 입

력된 요소들을 순차적으로 점검해야 하기 때문이다. LinkedList에 입력되어 있는 요소의

수가 많다면, 검색 결과를 얻는 데 오랜 시간이 소요될 것이다.

표5는 LinkedList의 속성을 보여주고 있다.

기본 용량 1 개

텅 비었을 때의 메모리 크기 48 bytes

오버헤드 24 bytes + 요소당 24 bytes 추가

1 만 개의 요소에 대한 오버헤드 ~240K

검색/추가/삭제 성능 O(n) — 입력된 요소의 수에 정비례하여 속도가 저하됨

표 5. LinkedList 객체의 특징

Java Collections: ArrayList

ArrayList는 List 인터페이스의 구현체로, 크기가 변할 수 있는 배열이다. JavaSE6 API 문

서에서 언급하는 ArrayList에 대한 내용은 다음과 같다.

ArrayList 는 순서가 정해져 있는 컬렉션 클래스이다.(시퀀스라고도 불린다.) 이

인터페이스를 이용할 경우, 리스트 내에 하나 하나의 요소들이 입력되는 위치에

대한 세밀한 제어가 가능해진다. 정해진 정수형의 인덱스를 이용하여 리스트 내의

Page 17: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

특정 요소에 접근하거나 검색할 수 있다. Set 인터페이스와는 달리 동일한 요소가

입력될 수 있다.

LinkedList와의 차이점은 ArrayList는 LinkedList$Entry 클래스 대신 Object의 배열을 사

용한다는 점이다. 그림 10을 보면 32bit Java 런타임에서 ArrayList가 최초 생성되었을 때

할당되는 메모리를 확인할 수 있다.

그림 10. 32bit Java 프로세스에서의 ArrayList 객체에 대한 메모리 배치 예

그림 10을 보면 ArrayList가 생성되었을 때, ArrayList 객체를 위해 32byte, 그리고 크기

가 10인 Object 객체 배열을 위해 56byte가 할당되어 총 88byte가 할당되는 것을 확인

할 수 있다.

표6에서 ArrayList의 속성을 확인할 수 있다.

기본 용량 10

텅 비었을 때의 메모리 크기 88 bytes

오버헤드 48 bytes + 새로운 요소 마다 4 bytes 추가

1 만 개의 요소에 대한 오버헤드 ~40K

검색/추가/삭제 성능 O(n) — 입력된 요소의 수에 정비례하여 속도가 저하됨

표 6. ArrayList 객체의 특징

다른 종류의 "collections"

일반적으로 사용되는 컬렉션 클래스들뿐만 아니라, StringBuffer 클래스도 일종의 컬렉션

으로 여겨질 수 있는데, 이는 StringBuffer가 문자열 데이터를 포함하고 관리하고 있고

그 방식이 여타 컬렉션 클래스와 비슷하기 때문이다. JavaSE6 API 문서에는 StringBuffer

클래스에 대해 다음과 같이 기술되어 있다.

Thread-Safe 한 변경 가능한 문자열이다. <중략> 모든 StringBuffer 클래스는 저장

용량이 있는데, StringBuffer 객체 내에 포함되어 있는 문자열이 저장 용량보다 크지

Page 18: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

않을 때에는 새로운 내부 버퍼 배열을 할당할 필요가 없다. 그 크기보다 커지면

내부 버퍼가 자동적으로 증가된다.

StringBuffer 클래스는 char 데이터의 배열을 관리하도록 구현되어 있다. 그림11를 통해

32bit Java 런타임 상에서 StringBuffer를 위해 메모리가 어떻게 할당되는지 확인할 수

있다.

그림 10. 32bit Java 프로세스에서의 StringBuffer 객체에 대한 메모리 배치 예

그림11에서 볼 수 있듯이, StringBuffer 객체가 최초 생성되면, StringBuffer를 위해

24byte를, 그리고 크기가 16인 char 배열을 위해 48byte가 할당되어, 총 72byte가 할당

되는 것을 확인할 수 있다.

다른 컬렉션 클래스들과 마찬가지로, StringBuffer도 기본 용량과 그 용량이 재조정되는

메커니즘을 갖고 있다. 표7에서 StringBuffer의 속성을 확인할 수 있다.

기본 용량 16

텅 비었을 때의 메모리 크기 72 bytes

오버헤드 24 bytes

1 만 개의 요소에 대한 오버헤드 24 bytes

검색/추가/삭제 성능 NA

표 7. StringBuffer 객체의 특징

Page 19: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

컬렉션 내의 빈 공간들

지금까지 봐 왔던, 여러 가지 컬렉션들의 오버헤드들이 메모리 오버헤드에 대한 모든

내용은 아니다. 지금까지의 예에서 각 컬렉션의 크기가 정확하게 측정될 수 있는 것처

럼 설명했었는데, 사실 대체로 그렇지 않다. 대부분의 컬렉션들은 초기 저장용량을 바탕

으로 생성되고, 데이터들이 생성된 컬렉션 안에 입력된다. 그렇다는 말은, 초기 저장용량

이 실제로 입력되는 데이터의 크기보다 더 크다면 그 차이만큼 오버헤드가 생긴다는 의

미가 된다.

StringBuffer의 예를 이용하여 생각해 보자. StringBuffer의 기본 용량은 16개의 char를

저장할 수 있고, 메모리로 얘기하면 그 크기는 72byte이다.19 StringBuffer가 처음 생성되

었을 때에는, 아무런 데이터를 입력하지 않았으니, 72byte에 어떤 데이터도 저장되어 있

지 않는 것이다. 이 상태에서, 예를 들어 “MY STRING”이라는 문자열을 입력한다면, 크기

16의 char 배열에 9개의 문자를 입력하는 것이 된다. 그림12에서 32bit Java 런타임에서

실행된다고 가정했을 때 지금까지의 메모리 상황을 확인할 수 있다.

그림 12. 32bit 프로세스 내에서 “MY STRING”이라는 문자열이 입력된 StringBuffer 객체의

메모리 상황

그림12에서 볼 수 있듯, 여전히 7개의 char 배열이 메모리에 할당되어 있지만 빈 상태로

남아 있다. 그리고 이에 대한 추가적인 메모리 오버헤드는 14byte가 된다.20 저장용량 16

에 대해 9개의 데이터만 입력되었으니 데이터 입력비율(fill ratio)은 0.5621이다. 컬렉션의

19 표7을 다시 한 번 확인하라.

20 원문에는 112byte의 오버헤드라고 나와있다. (- in this case an additional overhead of 112 bytes)

2byte 데이터인 char 7개가 낭비되고 있으면 14byte의 오버헤드이니 잘못된 정보이다. 역자가 생

각하기로는 112bit를 112byte로 잘못 적은 것은 것으로 보인다. (14 byte = 14 * 8bit = 112bit)

21 9 / 16 = 0.5625

Page 20: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

입력비율이 낮을수록 쓸데없이 할당되는 메모리가 커지게 되어 결과적으로 오버헤드가

더 커지게 된다.

Page 21: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

컬렉션 크기의 확장

컬렉션에 저장된 데이터의 양이 현재의 저장 용량에 가까워진 상태에서, 추가적인 데이

터 입력 요청이 발생하면 새로운 입력을 받아들일 수 있도록 컬렉션의 저장용량이 자동

적으로 확장된다. 이 과정에서 늘어난 저장용량에 비해 입력이 충분히 크지 않을 경우

입력비율이 낮아지고 결과적으로 메모리 오버헤드가 증가될 수 있다.

컬렉션마다 저장용량을 늘리는 알고리즘이 제각각이지만, 가장 일반적인 방식은 저장용

량을 두 배로 늘리는 것이다. 대표적으로 StringBuffer가 이 방식을 사용하는데, 위에서

말했던 예를 가지고 StringBuffer의 저장용량을 확장할 때 어떤 일이 일어나는지 알아보

자.

현재 생성되어 있는 StringBuffer– “MY STRING” 문자열을 포함하고 있는 –에 “OF TEXT”

라는 문자열을 추가해 보자. 그러면 이 StringBuffer 객체가 관리하게 되는 총 문자열은

“MY STRING OF TEXT”가 될 것이다. 그렇게 되면, StringBuffer의 기본 용량인 16을 넘는

문자들(17개의 문자)이 적재되어야 하기 때문에 String의 저장용량이 내부적으로 늘어나

게 된다. 그림 13을 보면 최종적인 메모리 사용 현황을 확인할 수 있다.

그림 13. 32bit 프로세스 내에서 “MY STRING OF TEXT”라는 문자열이 입력된 StringBuffer

객체의 메모리 상황

그림13에서 볼 수 있듯이, 총 32개의 char 배열에 17개 공간이 사용하게 되었고, 입력

비율은 0.53으로 22 오히려 줄어들었다. 하지만 빈 공간으로 인한 메모리 오버헤드는

30byte23로 오히려 늘어난 것을 알 수 있다.

물론, 작은 크기의 문자열이나 컬렉션에서는 입력비율이나 빈 공간으로 인해 발생하는

메모리 오버헤드가 그리 큰 문제가 아닐 수도 있다. 하지만, 크기가 커지면 커질수록 이

로 인한 문제들은 확실히 심각해진다. 예를 들어, StringBuffer 객체에 16MB의 문자열이

22 17 / 32 = 0.53125

23 역시 원문에는 240byte로 기술되어 있지만, 30byte = 240bit의 오타로 보인다.

Page 22: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

입력되어 있다면, char 배열은 32MB의 메모리 공간을 할당하게 될 것이고, 그로 말미암

아 16MB의 빈 공간이 쓸데없이 낭비될 수도 있다는 말이 된다.

Page 23: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

Java Collections: 요약

표8에 지금까지 다루었던 컬렉션 클래스들의 속성을 정리해 보았다.

Collection 성능 기본용량 비어있을 때

메모리

1 만개 요소

오버헤드

크기가 정확하게

계산되는가?

확장 알고리즘

HashSet O(1) 16 144B 360K No x2

HashMap O(1) 16 128 B 360K No x2

Hashtable O(1) 11 104 B 360K No x2+1

LinkedList O(n) 1 48 B 240K Yes +1

ArrayList O(n) 10 88 B 40K No x1.5

StringBuffer O(1) 16 72 B 24 No x2

표 8. 컬렉션 클래스 속성 요약

위 표에서 볼 수 있듯이, Hash 방식을 이용하는 컬렉션 클래스의 성능이 List 방식보다

더 좋지만 메모리 효율에서는 확실히 떨어지는 것을 알 수 있다.24 만약 큰 데이터를 관

리해야 하는데 성능이 가장 중요하게 고려되어야 한다면(예: 메모리 캐시), 메모리 오버헤

드를 무시하고 Hash 방식의 컬렉션을 이용하는 것이 바람직하다.

컬렉션에 저장되어야 하는 데이터의 양이 상대적으로 적고, 성능이 크게 문제되지 않는

다면 List 방식을 사용하는 것이 좋다. ArrayList와 LinkedList의 성능은 거의 동일하지만

메모리 사용 방식은 차이가 있다. 개별 입력 요소에 대한 메모리 사용은 ArrayList가 더

효율적이지만, 크기가 정확하게 계산되지 않기 때문에 쓸데없는 메모리를 사용할 수도

있다. 입력된 요소의 크기를 정확히 알지 못하는 경우에는 LinkedList를 사용하는 것이

바람직한데, 쓸데없는 빈 공간을 만들지 않기 때문이다. 즉, 입력할 요소들의 크기를 예

측할 수 있는지 여부가 ArrayList와 LinkedList 중 어떤 것을 선택할지 결정하는 기준이

될 것이다. 입력 데이터의 크기를 예측할 수 없으면 LinkedList를, 크기를 예측할 수 있

다면 ArrayList를 이용하는 것이 메모리 오버헤드를 줄이는 데 도움이 된다.

24 대부분의 경우에 메모리 효율과 알고리즘의 성능은 반비례하는 경향을 보인다.

Page 24: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

적절한 상황에 적절한 컬렉션을 사용함으로써 성능과 메모리 효율 사이에서 균형을 잡을

수 있다. 또, 입력비율을 최대화하고 사용되지 않는 빈 공간을 최소화 함으로써 메모리

효율을 최대한 향상시킬 수 있다.

Page 25: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

컬렉션의 실 사용 예 :

PlantsByWebSphere25와 WebSphere Application Server V7 표8에서 Hash 기반 컬렉션에 1만 개의 데이터를 입력하면 360KB의 메모리 오버헤드가

발생한다는 것을 본 적이 있을 것이다. 일반적으로 Java 어플리케이션들은 기가바이트 단

위의 힙 메모리를 사용하도록 설정되므로, 이 정도 오버헤드는 큰 문제가 되지 않는다고

생각할 수도 있지만, 사용되는 컬렉션의 양이 많아지고 데이터의 양이 늘어나면 문제가

될 수 있다.

표9는 PlantsByWebSphere에 5명의 사용자를 이용하여 부하 테스트를 했을 때 206MB의

Java 힙 메모리 중 컬렉션 객체들이 사용하는 부분만 발췌한 것이다.

컬렉션 유형 인스턴스의 수 총 컬렉션 오버헤드 (MB)

Hashtable 262,234 26.5

WeakHashMap 19,562 12.6

HashMap 10,600 2.3

ArrayList 9,530 0.3

HashSet 1,551 1.0

Vector 1,271 0.04

LinkedList 1,148 0.1

TreeMap 299 0.03

Total 306,195 42.9

표 9. PlantsByWebSphere 부하테스트 시 컬렉션 객체들이 사용하는 메모리 현황

이 표에서 알 수 있듯이 30만개가 넘는 서로 다른 컬렉션들이 사용되고, 그 과정에서 총

206MB의 메모리 중 42.9MB(21%에 달하는!)의 메모리가 허비되고 있는 것을 알 수 있다.

이 말은 적당한 상황에 적절한 컬렉션을 사용할 경우 메모리 효율이 더 좋아질 수 있음

을 의미한다.

25 PlantsByWebSphere는 IBM WebSphere에서 제공하는 샘플 웹 어플리케이션이다.

Page 26: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

Memory Analyzer 를 이용하여 입력비율 확인하기

IBM 서포트 어시스턴트에서 제공하는IBM 모니터링 및 진단 툴(메모리 분석 툴 –

Memory Analyzer)을 이용하면 컬렉션들의 메모리 사용 현황을 분석할 수 있

다.(Resources 메뉴를 확인하라.) 이 기능을 이용하여 메모리를 분석하면 어떤 컬렉션을

최적화할 것인지 결정할 수 있게 된다.

Memory Analyzer에서 컬렉션 분석 기능을 이용하려면 그림14에서 볼 수 있듯이 Open

Query Browser -> Java Collections 메뉴를 이용하면 된다.

그림 14. Memory Analyzer 에서 Java 컬렉션들의 입력비율을 분석하는 방법

그림14에서 보이는 메뉴 중, [Collection Fill Ratio] 기능을 이용하면 실제 필요한 메모리보

다 더 많은 메모리를 사용하고 있는 컬렉션을 쉽게 찾을 수 있다. 검색을 위해 몇 가지

값을 입력할 수 있는데 그 내용은 다음과 같다.

objects : 확인하고자 하는 객체(컬렉션)의 유형(Type)

segments : 객체들을 그룹짓기 위한 입력비율 값의 범위

검색 값으로 objects에 “java.util.Hashtable”을, segments로 10을 입력하고 조회를 수행

하면 그림 15와 같은 결과를 얻을 수 있다.

Page 27: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

그림 15. Memory Analyzer 를 이용하여 Hashtable 에 대한 입력비율을 조회한 결과

위 그림에서 java.util.Hashtable 인스턴스의 개수는 262,234개 인데, 그 중 127,016개

(48.4%)가 완전히 비어있고, 대부분 입력비율이 매우 적은 것을 알 수 있다.26

이제 어디서 사용되는 어떤 컬렉션이 문제가 있는지 확인하려면, 위 목록 중 하나에서

우클릭한 뒤, [incoming references] 메뉴 혹은 [outgoing references] 메뉴를 이용하면 된

다. [incoming references]는 어떤 객체가 컬렉션을 소유하고 있는지를 확인하는 데에,

[outgoing references]는 해당 컬렉션 내에 어떤 데이터가 입력되어 있는지를 확인하는

데에 사용된다.

그림16은 위 목록 중 입력비율이 0인 항목에 대한 [incoming references]를 확인하여 위

의 세 가지 항목을 확장한 것이다.

그림 16. 입력비율이 0 인 Hashtable 에 대한 incoming references 조회 결과

위 그림을 자세히 보면, javax.management.remote.rmi.NoCallStackClassLoader의

26 입력비율이 50%가 넘는 컬렉션의 개수는 고작 302개로 전체의 0.11%에 불과하다.

Page 28: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

packages, methodCache, fieldCache와 같은 필드가 입력비율이 0인 Hashtable인 것을

알 수 있다.

Memory Analyzer의 왼쪽 패널의 Attributes 뷰를 이용하면 그림17에서와 같이 선택된

Hashtable의 상세한 정보를 확인할 수 있다.

그림 17. Hashtable 인스턴스에 대한 상세 정보

위 그림에서 볼 수 있듯, 이 Hashtable은 기본 크기인 11이고 입력된 요소의 개수가 0

인 것을 알 수 있다.

이제 javax.management.remote.rmi.NoCallStackClassLoader 코드를 다음과 같은 방법

으로 최적화 할 수 있다.

- Hashtable 을 나중에 할당하기: 특정 Hashtable 이 대체로 텅 비어 있다면, 실제로

데이터를 입력하게 될 때 인스턴스를 생성하는 것이 합리적일 수 있다.27

- Hashtable 에 적절한 크기를 지정하기: Hashtable 을 생성할 때 적절한 크기 값을

입력한다면 쓸데없이 기본 크기를 위해 메모리를 허비할 필요가 없다.

코드가 동작하는 방식과 입력되는 데이터의 유형에 따라 두 가지 중 하나, 혹은 두 가지

모두를 이용하여 코드를 최적화할 수 있다.

27 이런 방식을 Lazy Initialization이라고도 한다. Lazy Initialization 방식을 이용할 경우, 위에서 설

명한 대로 메모리 사용을 최적화할 수 있는 반면, 실행 과정에서는 성능적으로 오히려 손해를 보

는 경우도 있다. 특히 복잡하고 무거운 객체에 대해 Lazy Initialize하는 것은 바람직하지 않을 수

있다. Lazy Initialization에 대한 상세한 내용은 http://en.wikipedia.org/wiki/Lazy_initialization를 참

조하라.

Page 29: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

PlantsByWebSphere 에서의 빈 컬렉션

표10은 PlantsByWebSphere 어플리케이션에서 텅 비어있는 것으로 분석된 컬렉션들의 목

록이다.

컬렉션 유형 인스턴스 수 비어있는 인스턴스 수 비어있는 비율

Hashtable 262,234 127,016 48.4

WeakHashMap 19,562 19,465 99.5

HashMap 10,600 7,599 71.7

ArrayList 9,530 4,588 48.1

HashSet 1,551 866 55.8

Vector 1,271 622 48.9

Total 304,748 160,156 52.6

표 10. PlantsByWebSphere 부하테스트 후 비어있는 컬렉션 목록

표10에서 알 수 있듯이 총 50%가 넘는 컬렉션들이 비어있고, 이 말은 적절한 컬렉션을

사용할 경우 메모리 효율을 증가시킬 수 있다는 것을 의미한다. 코드를 최적화하는 작업

은 여러 가지 수준에서 이루어질 수 있다. 즉, PlantsByWebSphere 코드, WebSphere 어플

리케이션 서버 코드, 그리고 컬렉션 코드 등에서 최적화가 이루어질 수 있다.

WebSphere 어플리케이션 서버는 버전 7에서 버전8로 업그레이드되면서, Java 컬렉션과

미들웨어 계층에서 메모리 효율을 향상시킬 수 있는 기능을 추가하였다. 예를 들어 위의

예에서 오버헤드의 상당 부분을 차지하는 것으로 확인된 java.util.WeakHashMap은 사실

약한 참조를28 처리하기 위해 포함하고 있는 java.lang.ref.ReferenceQueue 때문이었다.

그림18을 보면 32bit Java 런타임에서 WeakHashMap의 메모리 배치를 확인할 수 있다.

28 Weak reference. 말 그대로 약한 참조로, 특정 인스턴스에 대한 참조가 약한 참조만 남아 있다

면 해당 인스턴스는 Garbage Collection의 대상이 되어 다음 GC 시 메모리에서 제거된다.

Page 30: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

그림 18. 32bit Java 프로세스에서의 WeakHashMap 객체에 대한 메모리 배치 예

이 그림에서 알 수 있듯이, ReferenceQueue 인스턴스는 560byte를 보유(retain)하고 있

는데, 현재 WeakHashMap는 텅 비어 있으므로 이 인스턴스는 필요가 없는 상태이다.

PlantsByWebSphere 어플리케이션에는 19,465개의 텅 비어있는 WeakHashMap 인스턴

스가 생성되어 있는데, 불필요한 ReferenceQueue 인스턴스가 10.9MB의 메모리를 허비

하고 있는 것이다. WebSphere 어플리케이션 서버 버전8과 IBM의 Java7에서는

WeakHashMap 클래스에 대한 몇 가지 최적화를 수행하였는데, 먼저 WeakHashMap

객체가 포함하고 있는 ReferenceQueue가 실제로 필요할 때에 할당되도록 하여 텅 빈

상태로 메모리를 낭비하는 현상을 제거했다.

Page 31: From Java code to Java heap - Portfolio | ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/From-Java-code-to-Java... · 그림 1. 32bit Java 프로세스를 위한 메모리 배치

결론

하나의 어플리케이션 내에 놀랄 만큼 많은 수의 컬렉션이 사용되고, 어플리케이션이 복

잡하면 복잡할수록 더 많은 컬렉션이 사용된다. 적절한 위치에 적절한 컬렉션을 사용하

고, 적당한 크기를 지정하고, 그리고 적절한 시기에 생성함으로써 메모리 사용 효율을 높

일 수 있다. 이런 방법들은 대체로 설계 과정이나 개발 과정에서 적용되지만, Memory

Analyzer와 같은 도구를 이용하면 이미 만들어져 있는 어플리케이션이 메모리를 효율적

으로 이용하도록 분석하고 수정할 수도 있다.