오픈서베이의 새로운 결과 분석 서비스인 오픈애널리틱스를 개발하던 중 발생한 자바 메모리 이슈를 계기로 미시적 관점에서 JVM 메모리 할당을 분석/정리했습니다.
구체적으로, Integer / Long 등의 Object 형 타입과, ArrayList, / LinkedList / Set 등의 자료구조의 메모리 사용을 JDK코드 분석과 각종 도구를 통해 측정하고, 이를 효과적으로 사용하기 위한 방법을 탐구합니다.
2. 배경
- 평화롭게 오픈서베이의 신규 데이터 분석 서비스인 OpenAnalytics 를
개발하던 어느날, QA 과정에서 OutOfMemory 이슈가 등장함.
- 이건 과거의 내가 미래의 나에게 뭔가 잘못한 것이 분명했음.
- 과거의 나를 회상해보는 시간을 잠시 가져봄.
- (과거의 나) 설문 응답 데이터가 커봐야 얼마나 크겠어
- 많아봐야 Long / String 이백만건 정도인데,
String은 별로 없으니, Long만 따졌을 때, 8 * 2_000_000 = 16 MB
많이 봐줘서 10배 쳐줘도 160 MB니 -Xmx6g (= Heap 6GB)면 메모린 남아돌겠지?
- 역시 과거의 나는 믿을게 별로 못 된다는 사실부터 재확인
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. 코드로 추적해봅시다.
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] 코드 추적은 어디까지나 제 추측입니다. 틀린 부분 있으면 알려주세요
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. (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. - 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. (3) Locks
- The synchronization information for the object — that is, whether
the object is currently synchronized [1]
- ()V로 표시되던, wait / notify 등 동기화 관련 상태 필드
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
27. - Elements의 수에 비례해서 급격한 차이
- LinkedList 사용을 지양하고,
ArrayList.trimToSize() 사용권고
- new ArrayList()의 기본 크기 10이며,
크기를 초과할 경우, 임의의 크기 만큼
확장함
- 따라서, 실제 element 갯수보다 더 큰
크기를 확보하고 있을 가능성이 높음.
- trimToSize()는 이러한 Array 상의 null
element 제거해줌
그래프 출처: Numeron,
https://stackoverflow.com/a/7671021/1378965
ArrayList vs LinkedList
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. HashMap
- HashMap$Node
- int hash
- Object key
- Object value
- Node next
- Node[] table
- Set entrySet
- int size
- int modCount
- int threshold
- int loadFactor
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. 개선해보기(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. 개선해보기(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. 개선해보기(cont.)
메모리 상 Object 구성 분석 thanks to yourkit
- (상) JDK 기본
- (하) EclipseCollections
Eclipse Collections 도입 PoC 결과 비교 (2)
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. Q. 그렇다면 다른 프로젝트에서도 이런 드라마틱한 효과를 기대할 수 있을까요?
A. 그럴 수도 있고 아닐 수도 있습니다. 전술한 바와같이, OpenAnalytics의 드라마틱한 개선효과는
전적으로 OpenAnalytics의 구조적 특징과 Primitive 자료구조의 시너지 효과입니다.
본 자료는 Eclipse Collections 도입시, 성능 향상 가능성을 판단하는데,
도움을 줄 수 있는 사례 하나라고 생각합니다.
도입에 앞서서 충분한 검토와 Benchmark를 권고합니다.
개선해보기(cont.)
Eclipse Collections 도입 PoC 결과 비교 (4)
40. [없을 수도 있는 차회예고]
VTL: 오픈서베이에서 개발한 설문분석 전용 DSL 개발기
Shell pipe 스타일의 집합 연산 언어
끝!
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.