Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

JVM 메모리 해부학

5,898 views

Published on

오픈서베이의 새로운 결과 분석 서비스인 오픈애널리틱스를 개발하던 중 발생한 자바 메모리 이슈를 계기로 미시적 관점에서 JVM 메모리 할당을 분석/정리했습니다.

구체적으로, Integer / Long 등의 Object 형 타입과, ArrayList, / LinkedList / Set 등의 자료구조의 메모리 사용을 JDK코드 분석과 각종 도구를 통해 측정하고, 이를 효과적으로 사용하기 위한 방법을 탐구합니다.

Published in: Software
  • Login to see the comments

JVM 메모리 해부학

  1. 1. Greg’s Anatomy JVM 메모리 해부학 오픈서베이 이동훈(a.k.a Greg) greg.lee@opensurvey.co.kr leewin12@gmail.com 최초작성: 2020-07-18 외부공개: 2020-09-23
  2. 2. 배경 - 평화롭게 오픈서베이의 신규 데이터 분석 서비스인 OpenAnalytics 를 개발하던 어느날, QA 과정에서 OutOfMemory 이슈가 등장함. - 이건 과거의 내가 미래의 나에게 뭔가 잘못한 것이 분명했음. - 과거의 나를 회상해보는 시간을 잠시 가져봄. - (과거의 나) 설문 응답 데이터가 커봐야 얼마나 크겠어 - 많아봐야 Long / String 이백만건 정도인데, String은 별로 없으니, Long만 따졌을 때, 8 * 2_000_000 = 16 MB 많이 봐줘서 10배 쳐줘도 160 MB니 -Xmx6g (= Heap 6GB)면 메모린 남아돌겠지? - 역시 과거의 나는 믿을게 별로 못 된다는 사실부터 재확인
  3. 3. Java Object
  4. 4. new Integer(1) Q. JVM 상에서 위 Object의 크기는? (JVM 32 bit 가정)
  5. 5. A. 16 bytes(= 128 bits) - 네, 그렇습니다. 16 byte * 8 = 128 bit - 잠깐, 그런데, 실제 값은 고작 4 Byte고 나머진 뭔가요? - 그리고 Retained Size와 Shallow Size는 또 뭔가요? - Shallow Size는 객체 자체가 점유하고 있는 메모리 크기 - Retained Size는 직접 GC Root와 연결되지 않고, Shallow를 통해 간접 ref된 크기
  6. 6. 코드로 추적해봅시다. public class Object { private static native void registerNatives(); static { registerNatives(); } public native int hashCode(); ← Retained Size (2) public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; } public final class Integer extends Number implements Comparable<Integer> { private final int value; ← Shallow Size (4) } public abstract class Number implements java.io.Serializable {} [주1] 코드 추적은 어디까지나 제 추측입니다. 틀린 부분 있으면 알려주세요
  7. 7. 코드로 추적해봅시다. /* * jdk/src/share/native/java/lang/Object.c */ static JNINativeMethod methods[] = { {"hashCode", "()I", (void *)&JVM_IHashCode}, ← 2 {"wait", "(J)V", (void *)&JVM_MonitorWait}, ← 3 {"notify", "()V", (void *)&JVM_MonitorNotify}, ← 3 {"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll}, ← 3 {"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone}, Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls) { (*env)->RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(methods[0])); } JNIEXPORT jclass JNICALL Java_java_lang_Object_getClass(JNIEnv *env, jobject this) { …. return (*env)->GetObjectClass(env, this); ← 1 } 이하 중략...
  8. 8. (결론적) Integer Object의 구성[1] - 실제 primitive value 대비 Object의 크기 비율은 3:1 = 4배 0 32 64 96 128 160 (1) Class Pointer (2) Flags (3) Locks (5) size (4) int... 0 32 64 96 128 (1) Class Pointer (2) Flags (3) Locks (4) int - Array, (5) size가 추가됨 - 실제 primitive value 대비 Object의 크기 비율은 4:1 = 5배 (최악 가정)
  9. 9. (1) Class pointer - Class Type의 Memory Address - 따라서, 당연히 JVM의 bit 버전에 영향을 받음. - e.g. 윈도우 XP (x86)의 최대 인식 메모리는 최대 4G 였음 - (참고사항) JVM 64bit에 도입된 -XX:-UseCompressedOops -XX:-UseCompressedClassPointer 등에 영향 받음 - Oops: ordinary object pointer - 오픈서베이 표준 JVM인 Zulu8은 기본적으로 두 옵션이 모두 true로 켜져있음 - Oracle JVM에서는 false로 꺼져있음 - 자세한 사항은 https://wiki.openjdk.java.net/display/HotSpot/CompressedOops
  10. 10. - A collection of flags that describe the state of the object, including the hash code for the object if it has one, and the shape of the object (that is, whether or not the object is an array) [1] - 하지만 실제로, 코드상으로 확인되는 항목은 hashcode 뿐으로, 크기를 봤을 때 그 이상의 추가적인 flag가 존재하긴 어려워보임 - 뇌피셜입니다. (= 인터넷 문서상에서는 위처럼 기술되어 있지만, JDK 코드상에서 추가적 정보를 찾지 못함.) (2) Flags
  11. 11. (3) Locks - The synchronization information for the object — that is, whether the object is currently synchronized [1] - ()V로 표시되던, wait / notify 등 동기화 관련 상태 필드
  12. 12. (4) 타입별 크기 (Oracle JDK8 Spec, 32 bit)[2] - Primitive 한 경우 - boolean 혼란[4] - 첫 스펙 문서에서 크기를 정확히 지정하지 않았음. - 4 bytes (Oracle JDK8 Spec: Where Java programming language boolean values are mapped by compilers to values of Java Virtual Machine type int, the compilers must use the same encoding.) [2] - 1 bytes (Zulu8) - byte 1 byte - char 2 bytes ← byte < char 크기차 주의, low-level 처리시 실수 포인트 - short 2 bytes - int 4 bytes - long 8 bytes - float 4 bytes - double 8 bytes - Non-Primitive 한 경우 - Object 16 bytes - String ? bytes
  13. 13. new String(6) Q. JVM 상에서 위 Object의 크기는? (JVM 32 bit 가정)
  14. 14. A. 24 bytes + char array 0 32 64 96 128 160 192 224 (1) Class Pointer (2) Flags (3) Locks (4) char Array Pointer (5) hash (1) Class Pointer (2) Flags (3) Locks (4) size 1 / 2 3 / 4 5 / 6 - String은 내부에 다시 character array 와 hash로 구성
  15. 15. 확인해봅시다 by JOLJava Object Layout [3] $ java -jar jol-cli-0.11-full.jar internals java.lang.String java.lang.String object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) da 02 00 f8 (11011010 00000010 00000000 11111000) (-134216998) 12 4 char[] String.value [] 16 4 int String.hash 0 Instance size: 24 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 얼추 맞아 보이죠? 그런데 말입니다…
  16. 16. 코드로 추적해봅시다. Zulu8 (JDK8)
  17. 17. 코드로 추적해봅시다. AdoptOpenJ9 (JDK8) - int count가 있네요?
  18. 18. 코드로 추적해봅시다. AdoptOpenJ9 (JDK11) - char[] value → byte[] value - byte count → byte coder ??
  19. 19. 코드로 추적해봅시다. OpenJDK14 (JDK14) - coder: encoding을 표현하는 필드 - int hashcode → int hash (아무리 Compile time에 이름을 제거한다지만, 이런걸 줄이냐...)
  20. 20. 코드로 추적해봅시다. (결론) - 디테일은 JDK Vender와 JDK Version에 의해 다르다.
  21. 21. (참고) String.intern - String은 JVM에서 대우가 남다른 Type으로서, 별도의 저장영역 존재 - String.intern을 통해 JVM character pool에 Cache됨 (HashTable) - String `==`와 `equals`에 주의해야하는 이유
  22. 22. Java Collection
  23. 23. Java Collection - List - ArrayList - LinkedList - Map - HashMap - LinkedHashMap - TreeMap - Set - HashSet - LinkedHashSet - HashMap
  24. 24. ArrayList OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (5) 4 4 (object header) 00 00 00 00 (0) 8 4 (object header) 7e 2f 00 f8 (-134200) 12 4 int AbstractList.modCount 0 16 4 int ArrayList.size 0 20 4 java.lang.Object[] ArrayList.elementData [] Instance size: 24 bytes
  25. 25. LinkedList OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (5) 4 4 (object header) 00 00 00 00 (0) 8 4 (object header) 19 af 00 f8 (-134172903) 12 4 int AbstractList.modCount 0 16 4 int LinkedList.size 0 20 4 java.util.LinkedList.Node LinkedList.first null 24 4 java.util.LinkedList.Node LinkedList.last null 28 4 (loss due to the next object alignment) Instance size: 32 bytes 생각과 달리 크기가 작지 않습니다.
  26. 26. LinkedList - LinkedList$Node - Object item - Node next - Node prev - Node first - Node last - int size
  27. 27. - Elements의 수에 비례해서 급격한 차이 - LinkedList 사용을 지양하고, ArrayList.trimToSize() 사용권고 - new ArrayList()의 기본 크기 10이며, 크기를 초과할 경우, 임의의 크기 만큼 확장함 - 따라서, 실제 element 갯수보다 더 큰 크기를 확보하고 있을 가능성이 높음. - trimToSize()는 이러한 Array 상의 null element 제거해줌 그래프 출처: Numeron, https://stackoverflow.com/a/7671021/1378965 ArrayList vs LinkedList
  28. 28. HashMap OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) (5) 4 4 (object header) (0) 8 4 (object header) (-134203459) 12 4 java.util.Set AbstractMap.keySet null 16 4 java.util.Collection AbstractMap.values null 20 4 int HashMap.size 0 24 4 int HashMap.modCount 0 28 4 int HashMap.threshold 0 32 4 float HashMap.loadFactor 0.75 36 4 java.util.HashMap.Node[] HashMap.table null 40 4 java.util.Set HashMap.entrySet null 44 4 (loss due to the next object alignment) Instance size: 48 bytes
  29. 29. HashMap - HashMap$Node - int hash - Object key - Object value - Node next - Node[] table - Set entrySet - int size - int modCount - int threshold - int loadFactor
  30. 30. OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) (5) 4 4 (object header) (0) 8 4 (object header) (-134195647) 12 4 java.util.Set AbstractMap.keySet null 16 4 java.util.Collection AbstractMap.values null 24 4 int HashMap.modCount 0 20 4 int HashMap.size 0 28 4 int HashMap.threshold 0 32 4 float HashMap.loadFactor 0.75 36 4 java.util.HashMap.Node[] HashMap.table null 40 4 java.util.Set HashMap.entrySet null 44 1 boolean LinkedHashMap.accessOrder false 45 3 (alignment/padding gap) 48 4 java.util.LinkedHashMap.Entry LinkedHashMap.head null 52 4 java.util.LinkedHashMap.Entry LinkedHashMap.tail null Instance size: 56 bytes LinkedHashMap
  31. 31. OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) (5) 4 4 (object header) (0) 8 4 (object header) (-134179429) 12 4 java.util.Set AbstractMap.keySet null 16 4 java.util.Collection AbstractMap.values null 20 4 int TreeMap.size 0 24 4 int TreeMap.modCount 0 28 4 java.util.Comparator TreeMap.comparator null 32 4 java.util.TreeMap.Entry TreeMap.root null 36 4 java.util.TreeMap.EntrySet TreeMap.entrySet null 40 4 java.util.TreeMap.KeySet TreeMap.navigableKeySet null 44 4 java.util.NavigableMap TreeMap.descendingMap null Instance size: 48 bytes TreeMap
  32. 32. Set 사실 Java의 Set은 Map으로 구현되어 있음 정말 Wrapper임 (= 직관적으로 기대되는 메모리 상 이득은 전혀 없음) $ java -jar jol-cli-0.11-full.jar internals java.util.HashSet java.util.HashSet object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) (5) 4 4 (object header) (0) 8 4 (object header) (-134177913) 12 4 java.util.HashMap HashSet.map (object) Instance size: 16 bytes
  33. 33. 개선해보기
  34. 34. 개선해보기 - JVM의 Object는 생각보다 무겁다. - 적절한 자료구조를 선택하기 - Queue가 필요한 것이 아닌 이상, LinkedList는 쓰지 말자. - Set은 Map만큼 무겁다. - 필요한 만큼만 사용하기 - ArrayList.trimToSize() : List 상의 null element 최소화 - new HashMap(7) - JVM paramater 도입 -XX:+UseCompressOops - Zulu8에선 이미 기본 적용사항임 https://chriswhocodes.com/zulu_jdk8_options.html - 하지만, Oracle JDK8에서는 기본 값이 false임 - JVM parameter계의 explainshell 인 JaCoLine 도 겸사겸사 추천
  35. 35. 개선해보기(cont.) - Eclipse Collections - Primitive Collection 구현체들 중 가장 잘 유지보수 되고 있는 오픈소스 - 메모리 최적화된 Set 구현체도 보유 - 하지만, 항상 그렇듯이 먼저 문서를 확인하시고, 프로젝트 별로 벤치마크를 권고드립니다. 요즘 JMH라는 좋은 툴이 생겼어요. (하지만 JDK8은 추가작업이…) - 더 알아보기 - https://www.infoq.com/articles/eclipse-collections/ - https://www.infoq.com/articles/Refactoring-to-Eclipse-Collections/
  36. 36. 개선해보기(cont.) Eclipse Collections 도입 PoC 결과 비교(1) - 믿기 어려울 정도의 개선효과 - 십수회 반복 측정 결과이지만, 그래도 뭔가 잘못 측정된 것이 아닐까? 구현방법 소요시간 Obj 갯수 최대 메모리 사용량 (Peak지점 사용량) 종료기점 Stron Reachable Object 크기 JDK 기본 1,428,126 ms 224 M 6.4 Gb 3.6Gb Eclipse Collections 503,218 ms (65% 감소) 123 M (45% 감소) 4.8 Gb (25% 감소) 2.3 Gb (34% 감소)
  37. 37. 개선해보기(cont.) 메모리 상 Object 구성 분석 thanks to yourkit - (상) JDK 기본 - (하) EclipseCollections Eclipse Collections 도입 PoC 결과 비교 (2)
  38. 38. 개선해보기(cont.) ● 메모리 상 Object 구성(2)이 크게 달라진 것을 확인 할 수 있음 ○ EC.Immutable*List의 특징 ■ List가 Imuutable 하다는 점을 이용하여 다양한 최적화가 적용되어있음 ■ List 크기 불변성을 이용한 null element 수 최소화 ■ ImmutableSingleList부터 Decapleton까지 element 갯수에 따른 micro tuning ● Immutable 자료구조를 통해 기존 Defensive Copy 정책으로 인한 중복 Object 생성비용 절감 ● 메모리 상 Object 구성(2)에서 Object[]로 표현되던, 각종 자료 구조(ArrayList, Set) 내부에서 참조 중인 Object를 Primitive로 다이어트 ● (퍼포먼스 상 부가이득) Boxing / Unboxing 비용절약 ● (퍼포먼스 상 부가이득) 효율적인 메모리 활용을 통한 GC 소요시간 단축 결론: OpenAnalytics의 구조적 특징과 Primitive 자료구조의 시너지 효과 Eclipse Collections 도입 PoC 결과 비교 (3)
  39. 39. Q. 그렇다면 다른 프로젝트에서도 이런 드라마틱한 효과를 기대할 수 있을까요? A. 그럴 수도 있고 아닐 수도 있습니다. 전술한 바와같이, OpenAnalytics의 드라마틱한 개선효과는 전적으로 OpenAnalytics의 구조적 특징과 Primitive 자료구조의 시너지 효과입니다. 본 자료는 Eclipse Collections 도입시, 성능 향상 가능성을 판단하는데, 도움을 줄 수 있는 사례 하나라고 생각합니다. 도입에 앞서서 충분한 검토와 Benchmark를 권고합니다. 개선해보기(cont.) Eclipse Collections 도입 PoC 결과 비교 (4)
  40. 40. [없을 수도 있는 차회예고] VTL: 오픈서베이에서 개발한 설문분석 전용 DSL 개발기 Shell pipe 스타일의 집합 연산 언어 끝!
  41. 41. 참고문헌 [1] https://www.ibm.com/developerworks/java/library/j-codetoheap/index.html [2] https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.3 [3] https://openjdk.java.net/projects/code-tools/jol/ [4] https://stackoverflow.com/questions/383551/what-is-the-size-of-a-boolean-variable-in-java
  42. 42. 부록: JOL JOL-cli https://repo.maven.apache.org/maven2/org/openjdk/jol/jol-cli/0.11/jol-cli-0.11-full.jar $ java -jar jol-cli-0.11-full.jar Usage: jol-cli.jar <mode> [optional arguments]* Available modes: estimates: Simulate the class layout in different VM modes. externals: Show the object externals: the objects reachable from a given instance. footprint: Estimate the footprint of all objects reachable from a given instance heapdump: Consume the heap dump and estimate the savings in different layout strategies. heapdumpstats: Consume the heap dump and print the most frequent instances. idealpack: Compute the object footprint under different field layout strategies. internals: Show the object internals: field layout and default contents, object header shapes: Dump the object shapes present in JAR files or heap dumps. string-compress: Consume the heap dumps and figures out the savings attainable with compressed strings.
  43. 43. 적용해보기: http://www.mastertheboss.com/jboss-server/jboss-monitoring/monitoring-the-size-of-your-java-objects-with-java-object-layout com.idincu.analytics2.ast.value.ValueRow object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) (13) 4 4 (object header) (0) 8 4 (object header) (-133007067) 12 4 java.util.Set AbstractMap.keySet null 16 4 java.util.Collection AbstractMap.values null 20 4 int HashMap.size 48 24 4 int HashMap.modCount 48 28 4 int HashMap.threshold 48 32 4 float HashMap.loadFactor 0.75 36 4 java.util.HashMap.Node[] HashMap.table [(object), null, ... ] 40 4 java.util.Set HashMap.entrySet null 44 4 (loss due to the next object alignment) Instance size: 48 bytes 부록: JOL

×