[Java/JOL] Java Layout Object
1. Overview
JOL(Java Layout Object) Library 사용법
2. Description
Java 객체의 실제 크기를 Inspect 하기 위해서는 Instrumentation 을 활용할 수 있으나,
이는 Shallow size만 알아낼 수 있다.
실제 Size를 Heap dump보다 정확하게 추적할 수 있다고 소개하는 JOL 을 사용해보자.
3. Use
최신버전을 다운로드 하여 WEB-INF/lib 에 위치시킨다.
3.1 My App
아래와 같은 Business Java code 가 있다고 가정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ArrayList<byte[]> list = new ArrayList<byte[]>();
int addedNum = 500;
int addedByte = 1;
byte[] objectInSession = new byte[addedByte];
for(int i=0; i<addedNum; i++){
list.add(objectInSession);
}
HttpSession session = request.getSession(true);
ArrayList<byte[]> sList = (ArrayList<byte[]>)session.getAttribute("listSession");
if (sList == null){
sList = list;
}
else{
sList.addAll(list);
}
session.setAttribute("listSession", sList);
App 반복 요청 시, 매회 ArrayList로 이루어진 500 bytes data를 누적하여 Session에 저장한다.
3.2 Import JOL
이때, Session 에 저장되는 실제 size를 추적하기 위해,
다음의 JOL library import 한다.
1
2
3
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;
import org.openjdk.jol.vm.VM;
3.3 Specified Field Size in VM
System.out.println(VM.current().details());
호출 시
1
2
3
4
5
6
7
8
9
10
11
# VM mode: 64 bits
# Compressed references (oops): 3-bit shift
# Compressed class pointers: 3-bit shift
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Object alignment: 8 bytes
# ref, bool, byte, char, shrt, int, flt, lng, dbl
# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8
# Array base offsets: 16, 16, 16, 16, 16, 16, 16, 16, 16
byte가 memory에서 1 byte를 차지한다고 알 수 있다.
3.4 Shallow Size of ArrayList
System.out.println(ClassLayout.parseInstance(sList).toPrintable());
호출 시
1
2
3
4
5
6
7
8
9
10
11
12
java.util.ArrayList object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x000179d0
12 4 int AbstractList.modCount 500
16 4 int ArrayList.size 500
20 4 java.lang.Object[] ArrayList.elementData [[0], [0], ... more 400+
...
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
ClassLayout은 Object 또는 Class 자체의 Size(Shallow size)만을 계산한다고 하기 때문에, ArrayList.size=500 임에도 매우 작은 24 bytes 로 보여진다.
3.5 Shallow Size of Array in ArrayList
System.out.println(ClassLayout.parseInstance(sList.toArray()).toPrintable());
호출 시
1
2
3
4
5
6
7
[Ljava.lang.Object; object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00011840
12 4 (array length) 500
16 2000 java.lang.Object Object;.<elements> N/A
Instance size: 2016 bytes
toArray() 를 조사한 결과, Shallow Size 임에도 2016 bytes 라는 전체 실제 Size로 보인는 결과가 나온다.
3.6 Shallow Size of Single Object in ArrayList
System.out.println(ClassLayout.parseInstance(sList.toArray()[0]).toPrintable());
호출 시
1
2
3
4
5
6
7
8
9
[B object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x000007a8
12 4 (array length) 1
16 1 byte [B.<elements> N/A
17 7 (object alignment gap)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
byte[] objectInSession
Object의 Size는 24 bytes 이며, 내부에 new byte[addedByte];
으로 생성한 1 byte 가 확인된다.
여기까지의 내용으로는 모든 Object의 Size를 추적하여, 실제 Session data size를 확인할 수 있을 것 같았지만 난해하였고,
ClassLayout은 조사하는 Object 자체만의 Shallow Size를 보여준다.
3.7 Retained Size of ArrayList
GraphLayout을 이용하면, 첫 진입점부터 닿을 수 있는 Deep 한 곳까지의 모든 Size를 조사할 수 있다고 한다.
ClassLayout을 이용하면 Accurate Size를 얻을 수 없다.
System.out.println(GraphLayout.parseInstance(sList).toPrintable());
호출 시
1
2
3
4
5
6
java.util.ArrayList@4513a2d0d object externals:
ADDRESS SIZE TYPE PATH VALUE
7d56522c0 24 java.util.ArrayList (object)
7d56522d8 24 [B .elementData[0] [0]
7d56522f0 4520 (something else) (somewhere else) (something else)
7d5653498 2216 [Ljava.lang.Object; .elementData [[0], [0], ... more 400+
어디까지 닿았는지 모를 something else (4520 size) 외에 ArrayList (2216 size)가 확인된다.
최소한 App에서 생성한 Session data size는 2216 bytes 이상이 아닐까?
우리가 Session에 넣은 Object가 아니라 Session 자체를 조사하면 어떻게 되나?
3.8 Retained Size of HttpSession
System.out.println(GraphLayout.parseInstance(session).toPrintable());
호출 시
OOME 으로 죽었다.
System.out.println(GraphLayout.parseInstance(session).totalSize());
호출 시
92958432 , 즉 88 Mbytes 로 확인된다.
1 User가 생성한 1 Session의 순수 크기를 알고 싶지만, Retained 는 연결된 모든 Object를 추적하여서 그런지, 매우 큰 MB Size가 나왔다.
이 어플리케이션의 ArrayList를 걷어내고, 좀 더 단순한 구조에서 확인해보는 테스트가 필요해보인다.
그리하여, 다음과 같이 My App을 수정하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.ServletException;
import java.io.IOException;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;
import org.openjdk.jol.vm.VM;
@WebServlet("/SessionServlet")
public class SessionServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(true);
byte[] byteSession = (byte[]) session.getAttribute("byteSession");
byte[] _obj = new byte[500];
byte[] _tmp = null;
if (byteSession == null){
session.setAttribute("byteSession", _obj);
byteSession = (byte[]) session.getAttribute("byteSession");
}
else{
_tmp = new byte[_obj.length + byteSession.length];
System.arraycopy(_obj, 0, _tmp, 0, _obj.length);
System.arraycopy(byteSession, 0, _tmp, _obj.length, byteSession.length);
session.setAttribute("byteSession", _tmp);
byteSession = (byte[]) session.getAttribute("byteSession");
}
String log = "";
String et = "\n\r";
if(_obj!=null){
log += "_obj.length = " + _obj.length + et;
log += "ObjectSizeAgent.getObjectSize(_obj) = " + ObjectSizeAgent.getObjectSize(_obj) + et;
}
if(_tmp!=null){
log += "_tmp.length = " + _tmp.length + et;
log += "ObjectSizeAgent.getObjectSize(_tmp) = " + ObjectSizeAgent.getObjectSize(_tmp) + et;
}
if(byteSession!=null){
log += "byteSession.length = " + ((byteSession!=null) ? byteSession.length + et : "" + et);
log += "ObjectSizeAgent.getObjectSize(byteSession) = " + ObjectSizeAgent.getObjectSize(byteSession) + et;
}
log += "ObjectSizeAgent.getObjectSize(session) = " + ObjectSizeAgent.getObjectSize(session) + et;
System.out.println(log+et);
final long MEGABYTE = 1024L * 1024L;
long heapSize = Runtime.getRuntime().totalMemory() / MEGABYTE;
long heapMaxSize = Runtime.getRuntime().maxMemory() / MEGABYTE;
long heapFreeSize = Runtime.getRuntime().freeMemory() / MEGABYTE;
log = "";
log += "heapSize (MB) = " + heapSize + et;
log += "heapMaxSize (MB) = " + heapMaxSize + et;
log += "heapSize (MB) = " + heapFreeSize + et;
System.out.println(log);
// _obj
System.out.println("--- Layout : _obj ---");
System.out.println(ClassLayout.parseInstance(_obj).toPrintable());
System.out.println(GraphLayout.parseInstance(_obj).totalSize());
System.out.println(et);
// _tmp
if (_tmp != null){
System.out.println("--- Layout : _tmp ---");
System.out.println(ClassLayout.parseInstance(_tmp).toPrintable());
System.out.println(GraphLayout.parseInstance(_tmp).totalSize());
System.out.println(et);
}
// byteSession
System.out.println("--- Layout : byteSession ---");
System.out.println(ClassLayout.parseInstance(byteSession).toPrintable());
System.out.println(GraphLayout.parseInstance(byteSession).totalSize());
System.out.println(et);
}
}
ArrayList 등을 걷어내고, 순수 Byte Array 로만 Session에 ‘byteSession’ Key 의 Value 로 값을 저장한다.
3.9 Shallow Size of Byte Array
반복 호출 시마다 Session에 byte[] _obj = new byte[500];
만큼의 Data를 증분시킨다.
_obj
자체의 Shallow/Retained Size는 다음의 Java code 로 알 수 있다.
1
2
3
4
5
// _obj
System.out.println("--- Layout : _obj ---");
System.out.println("Shallow : " + ClassLayout.parseInstance(_obj).toPrintable());
System.out.println("Retained : " + GraphLayout.parseInstance(_obj).totalSize());
System.out.println(et);
_obj
자체의 입장에서는, 더 이상 닿을 곳이 없는 root 그 자체이기 때문에520 bytes
로 항상 동일하게 측정된다.
1
2
3
4
5
6
7
8
9
10
11
12
--- Layout : _obj ---
Shallow : [B object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x000007a8
12 4 (array length) 500
16 500 byte [B.<elements> N/A
516 4 (object alignment gap)
Instance size: 520 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Retained : 520
캡슐화를 위한 header(8+4=12 bytes)
_obj 배열 크기 Data 4 bytes
_obj 배열 Data 자체 500 bytes
그리고 ObjectAlignmentInBytes , Data 정렬을 위한 gap 으로 4 bytes 가 추가되어
_obj 객체 자체의 총크기는 520 bytes 가 된다.
3.10 What is ObjectAlignmentInBytes?
여기서 잠깐, ObjectAlignmentInBytes 를 살펴보면,
JVM에서는 Data를 Heap 에 저장할 때, 8 ~ 256 bytes 의 단위의 gap 을 유지하며 저장한다.
현재 나의 해당 설정값은, 8 bytes 이다.
1
2
$ java -XX:+PrintFlagsFinal | grep "ObjectAlignmentInBytes"
intx ObjectAlignmentInBytes = 8 {lp64_product}
Heap Memory에 Data가 올라갈 때, 8의 배수를 유지하도록 한다는 것이다.
이 값이, 더 커질 경우 어떤 장/단점이 있는지는 구글링 자료에 많으나 이해가 되지 않았다.
가령 위에서 살펴본 _obj Object 의 Size는 header + length + Data = 12 + 4 + 500 = 516 bytes 이다.
Heap 에 저장될 때, 8 bytes 의 배수 단위로 Data의 정렬이 이루어져야 하므로,
8 X 65 = 520 bytes
, 8의 65 배수로 Data가 정렬이 되어야 한다.
그리하여, Data 정렬을 위한 gap 으로 4 bytes 를 마저 추가한 것이다.
3.11 Shallow Size of byteSession
App에서 생성(_obj) 하여 Session에 집어넣을 때, byteSession (Session에 저장된 byte Array) 의 크기를 추적해본다.
추적 code는 다음과 같다.
1
2
3
4
5
// byteSession
System.out.println("--- Layout : byteSession ---");
System.out.println("Shallow : " + ClassLayout.parseInstance(byteSession).toPrintable());
System.out.println("Retained : " + GraphLayout.parseInstance(byteSession).totalSize());
System.out.println(et);
1회 호출 시에는, _obj Data와 다르지 않다.
1
2
3
4
5
6
7
8
9
10
11
12
--- Layout : byteSession ---
Shallow : [B object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000067e900d701 (hash: 0x67e900d7; age: 0)
8 4 (object header: class) 0x000007a8
12 4 (array length) 500
16 500 byte [B.<elements> N/A
516 4 (object alignment gap)
Instance size: 520 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Retained : 520
2회 연속 호출 시에는,
1
2
3
4
5
6
7
8
9
10
11
--- Layout : _tmp ---
Shallow : [B object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x000007a8
12 4 (array length) 1000
16 1000 byte [B.<elements> N/A
Instance size: 1016 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Retained : 1016
8 X 127 = 1016 Bytes
, 8의 127 배수로 정렬이 완료되므로 추가 Gap data가 없다.
3회 연속 호출 시에는,
1
2
3
4
5
6
7
8
9
10
11
12
--- Layout : byteSession ---
Shallow : [B object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000036a33af901 (hash: 0x36a33af9; age: 0)
8 4 (object header: class) 0x000007a8
12 4 (array length) 1500
16 1500 byte [B.<elements> N/A
1516 4 (object alignment gap)
Instance size: 1520 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Retained : 1520
다시금 4 bytes Gap 이 추가되었다.
4. Outcome
JOL Library 를 사용하여, 특정 또는 Class 자체가 JVM Heap Memory에 차지하는 실제 Size를 추적할 수 있음을 확인했다.
또한, Object Alignment Gap 에 대해서 알 수 있었다.
5. References
https://www.baeldung.com/jvm-measuring-object-sizes
https://github.com/openjdk/jol