Java Tip 130 : 데이터 크기를 알고 있습니까?

최근에 저는 인 메모리 데이터베이스와 유사한 Java 서버 애플리케이션 설계를 도왔습니다. 즉, 초고속 쿼리 성능을 제공하기 위해 수많은 데이터를 메모리에 캐싱하도록 설계를 편향 시켰습니다.

프로토 타입을 실행 한 후 디스크에서 파싱 및로드 된 데이터 메모리 공간을 프로파일 링하기로 결정했습니다. 그러나 만족스럽지 못한 초기 결과는 설명을 검색하도록 유도했습니다.

참고 : 리소스에서이 기사의 소스 코드를 다운로드 할 수 있습니다.

도구

Java는 의도적으로 메모리 관리의 여러 측면을 숨기므로 객체가 소비하는 메모리 양을 파악하려면 약간의 작업이 필요합니다. 이 Runtime.freeMemory()방법을 사용하여 여러 개체가 할당되기 전후의 힙 크기 차이를 측정 할 수 있습니다 . Ramchander Varadarajan의 "Question of the Week No. 107"(Sun Microsystems, 2000 년 9 월) 및 Tony Sintes의 "Memory Matters"( JavaWorld, 2001 년 12 월) 와 같은 여러 기사에서 그 아이디어를 자세히 설명합니다. 안타깝게도 이전 기사의 솔루션은 구현이 잘못된 Runtime방법을 사용하기 때문에 실패 하고 후자의 솔루션에는 자체 결함이 있습니다.

  • Runtime.freeMemory()JVM이 언제든지 (특히 가비지 콜렉션을 실행할 때) 현재 힙 크기를 늘리기로 결정할 수 있기 때문에 단일 호출 만으로는 충분하지 않습니다. 총 힙 크기가 이미 -Xmx 최대 크기가 아닌 Runtime.totalMemory()-Runtime.freeMemory()경우 사용 된 힙 크기로 사용해야합니다 .
  • 단일 Runtime.gc()호출을 실행하면 가비지 수집을 요청하기에 충분히 공격적이지 않을 수 있습니다. 예를 들어 객체 종료 자도 실행하도록 요청할 수 있습니다. 그리고 이후 Runtime.gc()수집이 완료 될 때까지 블록에 문서화되어 있지 않습니다, 인식 힙 크기가 안정 될 때까지 기다려야하는 좋은 아이디어이다.
  • 프로파일 링 된 클래스가 클래스 별 클래스 초기화 (정적 클래스 및 필드 이니셜 라이저 포함)의 일부로 정적 데이터를 생성하는 경우 첫 번째 클래스 인스턴스에 사용되는 힙 메모리에 해당 데이터가 포함될 수 있습니다. 첫 번째 클래스 인스턴스가 사용하는 힙 공간을 무시해야합니다.

이러한 문제를 고려 Sizeof하여 다양한 Java 코어 및 애플리케이션 클래스에서 스누핑하는 도구를 제시합니다 .

public class Sizeof {public static void main (String [] args) throws Exception {// 우리가 사용할 모든 클래스 / 메서드 워밍업 runGC (); usedMemory (); // 할당 된 객체에 대한 강력한 참조를 유지하는 배열 final int count = 100000; 개체 [] 개체 = 새 개체 [개수]; 긴 힙 1 = 0; // count + 1 객체를 할당하고 첫 번째 객체를 버립니다 (int i = -1; i = 0) 객체 [i] = object; else {개체 = null; // 워밍업 개체 삭제 runGC (); heap1 = usedMemory (); // 힙 스냅 샷 생성}} runGC (); long heap2 = usedMemory (); // 힙 스냅 샷 이후 : final int size = Math.round (((float) (heap2-heap1)) / count); System.out.println ( " 'before'힙 :"+ heap1 + ", 'after'heap :"+ heap2); System.out.println ( "힙 델타 :"+ (힙 2-힙 1) + ", {"+ 개체 [0].getClass () + "} 크기 ="+ 크기 + "바이트"); for (int i = 0; i <count; ++ i) 객체 [i] = null; 개체 = null; } private static void runGC () throws Exception {// 여러 메서드 호출을 사용하여 Runtime.gc () // 호출하는 데 도움이됩니다. for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () throws Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory ()-s_runtime.freeMemory (); } 개인 정적 최종 런타임 s_runtime = Runtime.getRuntime (); } // 클래스 끝나는 <카운트; ++ i) 객체 [i] = null; 개체 = null; } private static void runGC () throws Exception {// 여러 메서드 호출을 사용하여 Runtime.gc () // 호출하는 데 도움이됩니다. for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () throws Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory ()-s_runtime.freeMemory (); } 개인 정적 최종 런타임 s_runtime = Runtime.getRuntime (); } // 클래스 끝나는 <카운트; ++ i) 객체 [i] = null; 개체 = null; } private static void runGC () throws Exception {// 여러 메서드 호출을 사용하여 Runtime.gc () // 호출하는 데 도움이됩니다. for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () throws Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory ()-s_runtime.freeMemory (); } 개인 정적 최종 런타임 s_runtime = Runtime.getRuntime (); } // 클래스 끝gc () // 여러 메서드 호출 사용 : for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () throws Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory ()-s_runtime.freeMemory (); } 개인 정적 최종 런타임 s_runtime = Runtime.getRuntime (); } // 클래스 끝gc () // 여러 메서드 호출 사용 : for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () throws Exception {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory ()-s_runtime.freeMemory (); } 개인 정적 최종 런타임 s_runtime = Runtime.getRuntime (); } // 클래스 끝Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory ()-s_runtime.freeMemory (); } 개인 정적 최종 런타임 s_runtime = Runtime.getRuntime (); } // 클래스 끝Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory ()-s_runtime.freeMemory (); } 개인 정적 최종 런타임 s_runtime = Runtime.getRuntime (); } // 클래스 끝

Sizeof의 주요 방법은 runGC()usedMemory()입니다. 메서드를 더 공격적으로 만드는 것처럼 보이기 때문에 runGC()래퍼 메서드를 사용하여 _runGC()여러 번 호출 합니다. (이유는 잘 모르겠지만 메서드 호출 스택 프레임을 만들고 파괴하면 도달 가능성 루트 집합이 변경되고 가비지 수집기가 더 열심히 작동하도록 할 수 있습니다. 또한 힙 공간의 많은 부분을 사용하여 충분한 작업을 생성합니다. 가비지 수집기가 시작되는 것도 도움이됩니다. 일반적으로 모든 것을 수집하는 것은 어렵습니다. 정확한 세부 사항은 JVM 및 가비지 수집 알고리즘에 따라 다릅니다.)

내가 호출하는 위치를주의 깊게 기록하십시오 runGC(). 관심있는 모든 것을 인스턴스화하기 위해 heap1heap2선언 사이의 코드를 편집 할 수 있습니다 .

또한 Sizeof객체 크기를 인쇄하는 방법에 유의하십시오 . 모든 count클래스 인스턴스에 필요한 데이터의 전 이적 폐쇄를 count. 대부분의 클래스에서 결과는 소유 한 모든 필드를 포함하여 단일 클래스 인스턴스가 사용하는 메모리입니다. 이 메모리 풋 프린트 값은 얕은 메모리 풋 프린트를보고하는 많은 상용 프로파일 러에서 제공하는 데이터와 다릅니다 (예를 들어, 개체에 int[]필드 가있는 경우 메모리 사용량이 별도로 표시됨).

결과

이 간단한 도구를 몇 개의 클래스에 적용한 다음 결과가 우리의 기대와 일치하는지 확인하십시오.

참고 : 다음 결과는 Windows 용 Sun의 JDK 1.3.1을 기반으로합니다. Java 언어 및 JVM 사양에 의해 보장되는 것과 보장되지 않는 사항으로 인해 이러한 특정 결과를 다른 플랫폼이나 다른 Java 구현에 적용 할 수 없습니다.

java.lang.Object

글쎄요, 모든 물체의 뿌리는 제 첫 번째 경우였습니다. 의 경우 다음을 java.lang.Object얻습니다.

'이전'힙 : 510696, '이후'힙 : 1310696 힙 델타 : 800000, {class java.lang.Object} 크기 = 8 바이트 

그래서 평야 Object는 8 바이트를 사용합니다. 물론, 아무도 모든 인스턴스가 지원 기본 작업이 좋아하는 분야를 다니는해야하기 때문에 크기가 0이 될 것으로 기대 안 equals(), hashCode(), wait()/notify(), 등.

java.lang.Integer

동료들과 저는 종종 네이티브 intsInteger인스턴스 로 래핑 하여 Java 컬렉션에 저장할 수 있습니다. 메모리 비용은 얼마입니까?

'이전'힙 : 510696, '이후'힙 : 2110696 힙 델타 : 1600000, {class java.lang.Integer} 크기 = 16 바이트 

16 바이트 결과는 int값이 추가로 4 바이트에 들어갈 수 있기 때문에 예상보다 약간 나쁩니다 . 를 사용하면 Integer값을 기본 유형으로 저장할 수있는 경우에 비해 300 %의 메모리 오버 헤드가 발생합니다.

java.lang.Long

Long보다 많은 메모리를 차지해야 Integer하지만 그렇지 않습니다.

'이전'힙 : 510696, '이후'힙 : 2110696 힙 델타 : 1600000, {class java.lang.Long} 크기 = 16 바이트 

분명히 힙의 실제 개체 크기는 특정 CPU 유형에 대한 특정 JVM 구현에 의해 수행되는 저수준 메모리 정렬의 영향을받습니다. a Long는 8 바이트의 Object오버 헤드와 실제 long 값에 대해 8 바이트를 더한 것처럼 보입니다 . 반대로, Integer사용하지 않은 4 바이트 구멍이 있었는데, 필자가 사용하는 JVM이 8 바이트 단어 경계에서 개체 정렬을 강제하기 때문일 가능성이 큽니다.

배열

프리미티브 타입 배열을 가지고 노는 것은 부분적으로는 숨겨진 오버 헤드를 발견하고 부분적으로는 또 다른 인기있는 트릭을 정당화하는 데 도움이됩니다. 프리미티브 값을 객체로 사용하기 위해 크기 1 배열에 래핑하는 것입니다. Sizeof.main()반복 할 때마다 생성 된 배열 길이를 증가시키는 루프를 갖도록 수정 하면 배열을 얻을 수 int있습니다.

길이 : 0, {클래스 [I} 크기 = 16 바이트 길이 : 1, {클래스 [I} 크기 = 16 바이트 길이 : 2, {클래스 [I} 크기 = 24 바이트 길이 : 3, {클래스 [I} 크기 = 24 바이트 길이 : 4, {class [I} 크기 = 32 바이트 길이 : 5, {class [I} 크기 = 32 바이트 길이 : 6, {class [I} 크기 = 40 바이트 길이 : 7, {class [I} 크기 = 40 바이트 길이 : 8, {클래스 [I} 크기 = 48 바이트 길이 : 9, {클래스 [I} 크기 = 48 바이트 길이 : 10, {클래스 [I} 크기 = 56 바이트 

char어레이의 경우 :

길이 : 0, {클래스 [C} 크기 = 16 바이트 길이 : 1, {클래스 [C} 크기 = 16 바이트 길이 : 2, {클래스 [C} 크기 = 16 바이트 길이 : 3, {클래스 [C} 크기 = 24 바이트 길이 : 4, {class [C} 크기 = 24 바이트 길이 : 5, {class [C} 크기 = 24 바이트 길이 : 6, {class [C} 크기 = 24 바이트 길이 : 7, {class [C} 크기 = 32 바이트 길이 : 8, {클래스 [C} 크기 = 32 바이트 길이 : 9, {클래스 [C} 크기 = 32 바이트 길이 : 10, {클래스 [C} 크기 = 32 바이트 

위에서 8 바이트 정렬의 증거가 다시 나타납니다. 또한 피할 수없는 Object8 바이트 오버 헤드 외에도 기본 배열은 다른 8 바이트를 추가합니다 (이 중 최소 4 바이트가 length필드를 지원함 ). 그리고 사용 은 동일한 데이터의 변경 가능한 버전을 제외하고 int[1]Integer인스턴스에 비해 메모리 이점을 제공하지 않는 것으로 보입니다 .

다차원 배열

다차원 배열은 또 다른 놀라움을 제공합니다. 개발자는 일반적으로 int[dim1][dim2]수치 및 과학 컴퓨팅과 같은 구조를 사용 합니다. 에서 int[dim1][dim2]배열 인스턴스, 모든 중첩 된 int[dim2]배열은이다 Object그 자체이다. 각각은 일반적인 16 바이트 배열 오버 헤드를 추가합니다. 삼각형 또는 비정형 배열이 필요하지 않으면 순수한 오버 헤드를 나타냅니다. 어레이 차원이 크게 다를 때 영향이 커집니다. 예를 들어 int[128][2]인스턴스는 3,600 바이트를 사용합니다. int[256]인스턴스가 사용 하는 1,040 바이트 (동일한 용량)에 비해 3,600 바이트는 246 %의 오버 헤드를 나타냅니다. 의 극단적 인 경우 byte[256][1]오버 헤드 요소는 거의 19입니다! 동일한 구문이 스토리지 오버 헤드를 추가하지 않는 C / C ++ 상황과 비교하십시오.

java.lang.String

String먼저 다음과 같이 구성된 빈을 시도해 보겠습니다 new String().

'이전'힙 : 510696, '이후'힙 : 4510696 힙 델타 : 4000000, {class java.lang.String} 크기 = 40 바이트 

그 결과는 매우 우울합니다. 비어 있으면 String40 바이트가 사용됩니다. 20 개의 Java 문자를 수용 할 수있는 충분한 메모리입니다.

String콘텐츠로 s를 시도하기 전에 String인턴되지 않도록 보장하는 s 를 만드는 도우미 메서드가 필요합니다 . 다음과 같이 리터럴 만 사용합니다.

 object = "20 자 문자열"; 

이러한 모든 개체 핸들이 동일한 String인스턴스를 가리 키기 때문에 작동하지 않습니다 . 언어 사양은 그러한 동작을 지시합니다 ( java.lang.String.intern()방법 참조 ). 따라서 메모리 스누핑을 계속하려면 다음을 시도하십시오.

public static String createString (final int length) {char [] result = new char [length]; for (int i = 0; i <length; ++ i) 결과 [i] = (char) i; 새로운 문자열을 반환 (결과); }

String제작자 방법으로 무장 한 후 다음과 같은 결과를 얻었습니다.

길이 : 0, {class java.lang.String} 크기 = 40 바이트 길이 : 1, {class java.lang.String} 크기 = 40 바이트 길이 : 2, {class java.lang.String} 크기 = 40 바이트 길이 : 3, {class java.lang.String} 크기 = 48 바이트 길이 : 4, {class java.lang.String} 크기 = 48 바이트 길이 : 5, {class java.lang.String} 크기 = 48 바이트 길이 : 6, {class java.lang.String} 크기 = 48 바이트 길이 : 7, {class java.lang.String} 크기 = 56 바이트 길이 : 8, {class java.lang.String} 크기 = 56 바이트 길이 : 9, {class java.lang.String} 크기 = 56 바이트 길이 : 10, {클래스 java.lang.String} 크기 = 56 바이트 

결과는 String의 메모리 증가가 내부 char어레이의 증가를 추적 한다는 것을 분명히 보여줍니다 . 그러나 String클래스는 24 바이트의 오버 헤드를 추가합니다. 비어 있지 않은 String크기가 10 자 이하인 경우 유용한 페이로드 (각각 2 바이트 char+ 길이 4 바이트)에 비해 추가 된 오버 헤드 비용 은 100 ~ 400 %입니다.

물론 패널티는 애플리케이션의 데이터 배포에 따라 다릅니다. 어떻게 든 10 String자가 다양한 응용 프로그램 의 일반적인 길이를 나타내는 것으로 생각했습니다 . 구체적인 데이터 포인트를 얻기 위해 StringJDK 1.3.x와 함께 제공 되는 SwingSet2 데모 ( 클래스 구현을 직접 수정)를 계측하여 String생성 되는 s 의 길이를 추적 했습니다. 데모를 가지고 몇 분 후에 데이터 덤프에 약 180,000 Strings개가 인스턴스화 되었음을 보여줍니다 . 그것들을 크기 버킷으로 분류하면 내 기대가 확인되었습니다.

[0-10] : 96481 [10-20] : 27279 [20-30] : 31949 [30-40] : 7917 [40-50] : 7344 [50-60] : 3545 [60-70] : 1581 [ 70-80] : 1247 [80-90] : 874 ... 

맞습니다. 모든 String길이 의 50 % 이상 이 0-10 버킷에 빠졌습니다. 이는 String클래스 비 효율성 의 매우 뜨거운 지점입니다 !

실제로 Strings는 길이가 제안하는 것보다 훨씬 더 많은 메모리를 소비 할 수 있습니다. Strings에서 생성 된 StringBuffers (명시 적으로 또는 '+'연결 연산자를 통해)는 일반적으로 s의 용량이 16으로 시작 하기 때문에 char보고 된 String길이 보다 긴 길이의 배열을 가질 수 있습니다. StringBuffer, 다음 append()작업에서 두 배로 . 예를 들어, 2가 아닌 16 크기 createString(1) + ' 'char배열로 끝납니다 .

우리는 무엇을해야합니까?

"이 모든 것이 매우 좋지만 우리는 String자바에서 제공 하는 s 및 기타 유형 을 사용할 수밖에 없습니다. 그렇죠?" 나는 당신이 묻는 것을 들었습니다. 알아 보자.

래퍼 클래스