최근에 "Java Virtual Machine에 대한 심층적 이해"를 읽으면서 Java 객체의 메모리 레이아웃에 대해 더 잘 이해하게 되었습니다. 그래서 자연스럽게 Java가 메모리를 얼마나 사용하는지에 대한 매우 일반적인 질문이 떠올랐습니다. 객체 점유?
인터넷에서 아주 잘 이야기하는 블로그를 찾았습니다: http://yueyemaitian.iteeye.com/blog/2033046 거기에서 제공되는 수업도 매우 실용적입니다:
import java.lang.instrument.Instrumentation; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashSet; import java.util.Set; /** * 对象占用字节大小工具类 * * @author tianmai.fh * @date 2014-03-18 11:29 */ public class SizeOfObject { static Instrumentation inst; public static void premain(String args, Instrumentation instP) { inst = instP; } /** * 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、<br></br> * 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;<br></br> * 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小 <br></br> * * @param obj * @return */ public static long sizeOf(Object obj) { return inst.getObjectSize(obj); } /** * 递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小 * * @param objP * @return * @throws IllegalAccessException */ public static long fullSizeOf(Object objP) throws IllegalAccessException { Set<Object> visited = new HashSet<Object>(); Deque<Object> toBeQueue = new ArrayDeque<Object>(); toBeQueue.add(objP); long size = 0L; while (toBeQueue.size() > 0) { Object obj = toBeQueue.poll(); //sizeOf的时候已经计基本类型和引用的长度,包括数组 size += skipObject(visited, obj) ? 0L : sizeOf(obj); Class<?> tmpObjClass = obj.getClass(); if (tmpObjClass.isArray()) { //[I , [F 基本类型名字长度是2 if (tmpObjClass.getName().length() > 2) { for (int i = 0, len = Array.getLength(obj); i < len; i++) { Object tmp = Array.get(obj, i); if (tmp != null) { //非基本类型需要深度遍历其对象 toBeQueue.add(Array.get(obj, i)); } } } } else { while (tmpObjClass != null) { Field[] fields = tmpObjClass.getDeclaredFields(); for (Field field : fields) { if (Modifier.isStatic(field.getModifiers()) //静态不计 || field.getType().isPrimitive()) { //基本类型不重复计 continue; } field.setAccessible(true); Object fieldValue = field.get(obj); if (fieldValue == null) { continue; } toBeQueue.add(fieldValue); } tmpObjClass = tmpObjClass.getSuperclass(); } } } return size; } /** * String.intern的对象不计;计算过的不计,也避免死循环 * * @param visited * @param obj * @return */ static boolean skipObject(Set<Object> visited, Object obj) { if (obj instanceof String && obj == ((String) obj).intern()) { return true; } return visited.contains(obj); } }
이 코드를 사용하여 읽고 확인할 수 있습니다. 참고로 이 프로그램을 실행하려면 javaagent를 통해 Instrumentation을 주입해야 합니다. 자세한 내용은 원본 블로그를 참조하세요. 오늘은 Java 객체가 차지하는 바이트 수를 수동으로 계산하는 기본 규칙을 주로 요약합니다. 기본 기술로 get√가 저와 같은 Java 초보자에게 도움이 되기를 바랍니다.
소개에 앞서 Java 객체의 메모리 레이아웃인 객체 헤더(Header), 인스턴스 데이터(Instance Data) 및 정렬 패딩(Padding)을 간략하게 검토하겠습니다. 메모 . 또한 내 환경은 HotSpot 가상 머신과 64비트 Windows이므로 환경에 따라 결과가 다를 수 있습니다.
이제 다음 텍스트를 입력하십시오.
개체 헤더
개체 헤더는 32비트 시스템에서는 8바이트, 64비트 시스템에서는 16바이트를 차지합니다.
인스턴스 데이터
기본형의 메모리 사용량은 다음과 같습니다.
기본형 메모리 필요량(바이트)
부울 1
바이트 1
짧은 2
문자 2
int 4
float 4
long 8
double 8
참조 유형은 32비트 시스템에서 각각 4바이트, 64비트 시스템에서 각각 8바이트를 차지합니다.
정렬 패딩
HotSpot의 정렬은 8바이트 정렬입니다.
(객체 헤더 + 인스턴스 데이터 + 패딩) % 8은 0 및 0과 같습니다. <= 패딩 < ;
포인터 압축
객체가 차지하는 메모리 크기는 VM 매개변수 UseCompressedOops의 영향을 받습니다.
1) 개체 헤더에 미치는 영향
켜기(-XX:+UseCompressedOops) 개체 헤더 크기는 12바이트(64비트 시스템)입니다.
static class A { int a; }
객체 A가 차지하는 메모리:
포인터 압축 끄기: 16+4=20은 8의 배수가 아니므로 +padding/4= 24
포인터 압축을 켭니다. 12+4=16은 이미 8의 배수이므로 패딩이 필요하지 않습니다.
2) 참조 유형에 미치는 영향
64비트 시스템에서 참조 유형은 8바이트를 차지하며, 포인터 압축 시 4바이트를 차지합니다.
static class B2 { int b2a; Integer b2b; }
B2 객체 메모리 사용량:
포인터 압축 끄기: 16+4+8=28은 8의 배수가 아니므로 +padding/4 =32
포인터 압축 활성화: 12+4+4=20은 8의 배수가 아니므로 +padding/4=24
배열 개체
64비트 시스템에서 배열 개체의 개체 헤더는 24바이트를 차지하며, 압축이 활성화된 후에는 16바이트를 차지합니다. 일반 객체에 비해 메모리를 더 많이 차지하는 이유는 배열의 길이를 저장하기 위해 추가 공간이 필요하기 때문입니다.
먼저 new Integer[0]이 차지하는 메모리 크기를 고려하세요. 길이는 객체 헤더의 크기인 0입니다.
압축이 켜지지 않음: 24bytes
압축을 켠 후: 16바이트
그런 다음 새 정수를 계산합니다. 1], new Integer[2 ], new Integer[3] 및 new Integer[4]는 쉽습니다.
압축 없음:
开启压缩:
拿new Integer[3]来具体解释下:
未开启压缩:24(对象头)+8*3=48,不需要padding;
开启压缩:16(对象头)+3*4=28,+padding/4=32,其他依次类推。
自定义类的数组也是一样的,比如:
static class B3 { int a; Integer b; }
new B3[3]占用的内存大小:
未开启压缩:48
开启压缩后:32
复合对象
计算复合对象占用内存的大小其实就是运用上面几条规则,只是麻烦点。
1)对象本身的大小
直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小; 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小。
static class B { int a; int b; } static class C { int ba; B[] as = new B[3]; C() { for (int i = 0; i < as.length; i++) { as[i] = new B(); } } }
未开启压缩:16(对象头)+4(ba)+8(as引用的大小)+padding/4=32
开启压缩:12+4+4+padding/4=24
2)当前对象占用的空间总大小
递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小。
递归计算复合对象占用的内存的时候需要注意的是:对齐填充是以每个对象为单位进行的,看下面这个图就很容易明白。
现在我们来手动计算下C对象占用的全部内存是多少,主要是三部分构成:C对象本身的大小+数组对象的大小+B对象的大小。
未开启压缩:
(16 + 4 + 8+4(padding)) + (24+ 8*3) +(16+8)*3 = 152bytes
开启压缩:
(12 + 4 + 4 +4(padding)) + (16 + 4*3 +4(数组对象padding)) + (12+8+4(B对象padding))*3= 128bytes
大家有兴趣的可以试试。
实际工作中真正需要手动计算对象大小的场景应该很少,但是个人觉得做为基础知识每个Java开发人员都应该了解,另外:对自己写的代码大概占用多少内存,内存中是怎么布局的应该有一个直觉性的认识。