最近、「Java 仮想マシンの詳細」を読んで、Java オブジェクトのメモリ レイアウトについてより深く理解できるようになりました。そこで、Java オブジェクトがどれくらいのメモリを占有するのかという非常に一般的な質問が自然に頭に浮かびました。 ?
インターネット上で非常に優れたブログを見つけました: http://yueyemaitian.iteye.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 を介してインストルメンテーションを挿入する必要があることに注意してください。詳細については、元のブログを参照してください。今日は主に、Java オブジェクトが占有するバイト数を手動で計算するための基本的なルールをまとめます。基本的なスキルとして、私のような Java 初心者に役立つことを願っています。
概要を説明する前に、Java オブジェクトのメモリ レイアウト (ヘッダー、インスタンス データ、パディング) について簡単に説明します。詳細については、私の読書メモを参照してください。さらに、環境によっては結果が異なる場合があります。私の環境は HotSpot 仮想マシンと 64 ビット Windows です。
次にテキストを入力します:
オブジェクトヘッダー
オブジェクトヘッダーは、32 ビット システムでは 8 バイト、64 ビット システムでは 16 バイトを占めます。
インスタンスデータ
プリミティブ型のメモリ使用量は以下の通りです:
Primitive Type Memory Required(bytes)
boolean 1
byte 1
short 2
char 2
int 4
float 4
long 8
double 8
参照型は、32 ビット システムではそれぞれ 4 バイト、64 ビット システムではそれぞれ 8 バイトを占有します。
アライメントされたパディング
HotSpotのアライメントは8バイトアライメントです:
(オブジェクトヘッダー + インスタンスデータ + パディング) % 8は0と0に等しい <= パディング < 8
ポインタ圧縮
オブジェクトによって占有されるメモリサイズは VM パラメータ UseCompressedOops の影響を受けます。
1) オブジェクトヘッダーへの影響
(-XX:+UseCompressedOops)をオンにすると、オブジェクトヘッダーのサイズは12バイト(64ビットマシン)になります。
static class A { int 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 バイトを占有します圧縮が有効になった後。通常のオブジェクトよりも多くのメモリを消費する理由は、配列の長さを格納するために余分なスペースが必要になるためです。
まず、新しい Integer[0] が占有するメモリ サイズを考慮します。長さは 0、オブジェクト ヘッダーのサイズです:
圧縮なし: 24 バイト
圧縮をオンにした後: 16 bytes
その後、 new Integer[1]、new Integer[2]、new Integer[3]、new Integer[4] を計算するのは簡単です:
Compression はオンになっていません:
开启压缩:
拿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开发人员都应该了解,另外:对自己写的代码大概占用多少内存,内存中是怎么布局的应该有一个直觉性的认识。