Java의 String 객체의 메모리 사용량

인터넷에서 자료를 찾다가, 우연히 다른 사람의 블로그를 방문하였습니다. 그 블로그에서는 다음의 두 프로그램이 JVM 메모리 사용량 관점에서 볼 때 너무 상이한 결과가 나온다면서 의아해하더군요.

Program 1

String str = "0123456789";
List list = new ArrayList(100);
long realSize = 0;
for (int i = 1; i < 100000000; i++) {
  String data = “0123456789” + str;
  list.add(data);

  realSize += data.getBytes().length
  if (i % 10000 == 0)
    System.out.println("dataCount=" + i + ",dataSize=" + realSize);
}

간단한 이 Java 프로그램을 -Xms256M -Xmx256M의 옵션으로 실행하면, 약 300만개(3,000,000)의 String을 ArrayList에 저장하고 out of memory 에러가 발생합니다. 즉, 256MB 정도의 메모리를 사용하는 Java 프로그램은 문자가 20개인 String 객체를 300만개 정도 만들어서 다룰 수 있습니다.

반면에 다음의 프로그램을 보시죠.

Program 2

String str = "0123456789";
List list = new ArrayList(100);
long realSize = 0;
for (int i = 1; i < 100000000; i++) {
  String data = new String("01234567890123456789"); // 요기 한줄만 다릅니다.
  list.add(data);
  realSize += data.getBytes().length
  if (i % 10000 == 0)
   System.out.println("dataCount=" + i + ",dataSize=" + realSize);
}

이 프로그램2는 프로그램1과 동일한 것처럼 보입니다. 프로그램1과 똑같이 20개의 문자열을 만들어서 ArrayList에 차곡차곡 넣어서 테스트를 하는 거죠. 하지만, 이 프로그램을 실행시키면, 1번과는 다른 결과가 나옵니다. 프로그램 1번을 실행할 때와 동일한 크기의 메모리 옵션인 -Xms256M -Xmx256M로 실행시키면, 20개짜리 문자를 가진 String을 850만개나 생성할 수가 있습니다. 1번과 비교하면 거의 3배에 가까운 String 객체를 생성할 수가 있습니다.

왜 이런 차이가 나올까요?
String a = "0123456789" + str 이 문제일까요?
Java의 성능에 대하여 조금이라도 관심이 있으신 분은, 이런 식으로 문자열 만들지 말라는 말을 많이 들었을 겁니다.
대신에 StringBuffer를 사용하는 게 성능 및 메모리 관리 좋다는 얘기 정도는 아실 겁니다. 혹시나 이 statement가 메모리를 과다하게 사용하는 것일까요? 근데, 아무리 생각해도 이게 메모리를 과다하게 사용하는 것에 대한 이유는 아닌 것 같습니다.

String a = "0123456789" + str; 이 프로그램 문장은
String a = new String("01234567890123456789"); 이 문장보다는 중간에 불필요한 객체를 더 생성하는 것은 사실입니다. 하지만, 그 객체는 곧 가비지 컬렉션 대상이 돼서 그 객체가 점유하고 있던 메모리는 반환됩니다. 이 부분을 좀 더 설명해 보겠습니다.

  String a = "0123456789" + str;

이 문장은 for loop 안에서 실행됩니다.
다음의 반복인 셈이죠.

String a = "0123456789" + str;

a = "0123456789" + str;

a = "0123456789" + str;

...


각 문장의 결과인 20개의 문자를 가진 String은 ArrayList에 추가되므로 이는 가비지 컬렉션 대상이 되지 않습니다. 반면에 중간에 생성된 불필요한 객체들은 그 객체들에 대한 레퍼런스가 없기때문에 가비지 컬렉션 때 사라집니다.

그 블로그에서는 다음과 같은 이유에서 프로그램 2가 옳다고 생각하더군요.

  1.  20개짜리 글자 850만개니까 170MB (= 850만 x 20) 사용
  2. ArrayList에서 String 객체를 레퍼런스하는데, 한 레퍼런스 당 4바이트니까,  35MB 사용 (= 850만 * 4B)
  3. 기타 다른 용도로 사용하는 메모리가 어느 정도 있음.
그래서 250MB의 메모리와 비슷한 용량이 나온다는 게, 그 블로그의 분석이었습니다.

이쯤 되면, 당연히 Java의 가비지 컬렉션 기능에 대하여 의심을 가지지 않을 수가 없습니다. 프로그램 1과 프로그램 2는 중간에 약간의 다른 모습을 보일지라도, 크게 보면, 20개의 문자를 가진 String 객체를 계속 생성하여 저장하는 동일한 프로그램인데 말이죠.

결론을 말씀 드리면 프로그램 1이 더 정확한 결과를 보여주고, 프로그램 2는 부정확한 프로그램입니다. 즉, 프로그램 2는 Java의 메모리 용량에 몇 개의 문자열이 들어갈 수 있는가를 테스트하는데 부적합합니다.

이유를 설명하겠습니다.
1. String 객체는 문자열의 갯수보다 더 큰 용량을 차지합니다.
  String은 객체이기때문에, primitive type보다는 부가 정보가 더 있습니다.
  문자열 갯수 용량 + 20 바이트를 사용합니다.
  20 바이트는 5개의 필드로 구성되어 있습니다.
  - 객체 헤더 4 바이트
  - 문자열에 대한 레퍼런스 4 바이트
  - 문자열에 offset 4 바이트
  - 문자열의 길이 필드 4 바이트
  - 해쉬값 필드 4 바이트

2. String은 자신이 직접 문자열을 갖고 있지 않고, char[] 로 갖고 있습니다.
   Java에서 어레이는 객체로 취급됩니다.

3. Java에서의 char는 2 byte입니다.
   그리고, String 객체는 문자열을 char[]에 저장합니다.

이것으로 계산을 해보면, 10개짜리 문자를 가진 String 객체 하나는 44 (= 20 + 4 + 10x2) 바이트의 용량을 차지합니다. 여기서 4 바이트는 char[]도 객체이므로, 객체 헤더가 필요합니다. 20개짜리 문자열이라면, 64 (= 20 + 4 + 20x2) 바이트를 차지합니다.

이렇게 해서 프로그램 1의 결과로 계산을 해보면,
1) 20개짜리 문자를 가진 String 300만개의 용량 = 64 x 300만 = 198MB
2) ArrayList에서의 300만개의 String 에 대한 레퍼런스 = 4 x 300만 = 12MB

동일하게 프로그램 2의 결과로 계산을 해보면,
1) 20개짜리 문자를 가진 String 850만개의 용량 = 64 x 850만 = 544MB
2) ArrayList에서의 850만개의 String 에 대한 레퍼런스 = 4 x 850만 = 35MB

그리고, 계산에서 간과한 게 있습니다.

4. ArrayList는 그 용량이 초과하는 데이터를 저장하게 되면, 기존의 용량보다 1.5배로 증가시킵니다.
   즉, 100개로 정의된 ArrayList에 101번째 데이터가 입력되면, 150개로 용량을 증가시킵니다. 이렇게 되면, 실제로 저장된 객체 수보다 많은 레퍼런스를 갖게 됩니다.

그렇다면, 프로그램 2는 어떻게 그런 결과를 낳았을까요?

5. Java에서의 new String("abc")나 new String(str) 은 문자열을 복사하지 않고, 기존의 문자열을 그대로 사용합니다.

  프로그램 2에서의 String a = new String("01234567890123456789"); 문장은
  String 객체는 만들어 내지만 그 실제 문자열은 복사하지 않고 공유를 합니다. 즉, 40바이트를 덜 사용합니다.

이렇게 해서, 프로그램 2번의 결과로 계산을 해보면,
  1. String 850만 개의 용량 = 24 x 850만 = 204MB
  2. ArrayList에서의 850만개의 String 에 대한 레퍼런스 = 4 x 850만 = 35MB


ps.
1. JVM 실행 시에 사용할 힙(heap) 메모리의 크기를 다음과 같은 옵션을 사용하여 지정할 수가 있습니다.
   옵션 -Xms 는 초기 프로그램 실행 시의 힙 메모리 크기며,
   옵션 -Xmx 는 최대 힙 메모리 크기를 의미합니다.
   "-Xms256M -Xmx256M" 이 옵션은 시작 시점과 최대 힙 메모리 크기를 256MB로 한정한다는 의미가 됩니다.

2. 통상 시작 및 최대 힙 메모리 크기를 동일한 값을 사용합니다.
    동일한 값일 때 가장 좋은 성능이 나오며 이유는 초기가 최대보다 작으면 최대로 메모리가 확장될 때 오버헤드가 걸리기 때문입니다.

댓글

럭정님의 메시지…
도움 많이 되었습니다!!

이 블로그의 인기 게시물

리틀의 법칙 – Little’s law

true, false, positive, negative – TP/TN/FN/FP