java一个对象占用多少字节?
java一个对象占用多少字节?_zzx410527的专栏-CSDN博客_java对象头占几个字节最近在读《深入理解Java虚拟机》,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存?想弄清楚上面的问题,先补充一下基础知识。1、JAVA 对象布局在 HotSpot虚拟机中,对象在内存中的存储的布局可以分为三块区域:对象头(Head
java一个对象占用多少字节?_zzx410527的专栏-CSDN博客_java对象头占几个字节
-
最近在读《深入理解Java虚拟机》,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存?
想弄清楚上面的问题,先补充一下基础知识。
1、JAVA 对象布局
在 HotSpot虚拟机中,对象在内存中的存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)
1.1对象头(Header): Java中对象头由 Markword + 类指针kclass(该指针指向该类型在方法区的元类型) 组成。 普通对象头在32位系统上占用8bytes,64位系统上占用16bytes。64位机器上,数组对象的对象头占用24个字节,启用压缩之后占用16个字节。
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在64位的虚拟机(未开启压缩指针)为64bit。 Markword: 类指针kclass: kclass存储的是该对象所属的类在方法区的地址,所以是一个指针,默认Jvm对指针进行了压缩,用4个字节存储,如果不压缩就是8个字节。 关于Compressed Oops的知识,大家可以自行查阅相关资料来加深理解。
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,这块占用4个字节。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 (并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考对象的访问定位)
1.2实例数据(Instance Data) 实例数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。也就是说,除去静态变量和常量值放在方法区,非静态变量的值是随着对象存储在堆中的。 因为修改静态变量会反映到方法区中class的数据结构中,故而推测对象保存的是静态变量和常量的引用。
1.3对齐填充(Padding) 用于确保对象的总长度为8字节的整数倍。 HotSpot要求对象的总长度必须是8字节的整数倍。由于对象头一定是8字节的整数倍,但实例数据部分的长度是任意的。因此需要对齐补充字段确保整个对象的总长度为8的整数倍。
2、Java数据类型有哪些
-
基础数据类型(primitive type)
-
引用类型 (reference type)
2.1基础数据类型内存占用如下 2.2引用类型 内存占用如下: 引用类型跟基础数据类型不一样,除了对象本身之外,还存在一个指向它的引用(指针),指针占用的内存在64位虚拟机上8个字节,如果开启指针压缩是4个字节,默认是开启了的。
2.3字段重排序 为了更高效的使用内存,实例数据字段将会重排序。排序的优先级为: long = double > int = float > char = short > byte > boolean > object reference 如下所示的类
class FieldTest{ byte a; int c; boolean d; long e; Object f; } 1234567
将会重排序为(开启CompressedOops选项):
OFFSET SIZE TYPE DESCRIPTION 16 8 long FieldTest.e 24 4 int FieldTest.c 28 1 byte FieldTest.a 29 1 boolean FieldTest.d 30 2 (alignment/padding gap) 32 8 java.lang.Object FieldTest.f 1234567
3、验证
讲完了上面的概念,我们可以去验证一下。 3.1有一个Fruit类继承了Object类,我们分别新建一个object和fruit,那他们分别占用多大的内存呢?
class Fruit extends Object { private int size; } Object object = new Object(); Fruit f = new Fruit(); 123456
先来看object对象,通过上面的知识,它的Markword是8个字节,kclass是4个字节, 加起来是12个字节,加上4个字节的对齐填充,所以它占用的空间是16个字节。 再来看fruit对象,同样的,它的Markword是8个字节,kclass是4个字节,但是它还有个size成员变量,int类型占4个字节,加起来刚好是16个字节,所以不需要对齐填充。
那该如何验证我们的结论呢?毕竟我们还是相信眼见为实!很幸运Jdk提供了一个工具jol-core可以让我们来分析对象头占用内存信息。具体使用参考 jol的使用也很简单: 打印头信息
public static void main(String[] args) { System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable()); System.out.print(ClassLayout.parseClass(Object.class).toPrintable()); } 1234
输出结果
com.zzx.algorithm.tst.Fruit object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Fruit.size N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 123456789101112
可以看到输出结果都是16 bytes,跟我们前面的分析结果一致。
3.2 除了类类型和接口类型的对象,Java中还有数组类型的对象,数组类型的对象除了上面表述的字段外,还有4个字节存储数组的长度(所以数组的最大长度是Integer.MAX)。所以一个数组对象占用的内存是 8 + 4 + 4 = 16个字节,当然这里不包括数组内成员的内存。 我们也运行验证一下。
public static void main(String[] args) { String[] strArray = new String[0]; System.out.println(ClassLayout.parseClass(strArray.getClass()).toPrintable()); } 1234
输出结果:
[Ljava.lang.String; object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 16 (object header) N/A 16 0 java.lang.String String;.<elements> N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 123456
输出结果object header的长度也是16,跟我们分析的一致。 3.3 接下来看对象的实例数据部分: 为了方便说明,我们新建一个Apple类继承上面的Fruit类
public class Apple extends Fruit { private int size; private String name; private Apple brother; private long create_time; } 1234567
// 打印Apple的对象分布信息
System.out.println(ClassLayout.parseClass(Apple.class).toPrintable()); 1
// 输出结果
com.zzx.algorithm.tst.Apple object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Fruit.size N/A 16 8 long Apple.create_time N/A 24 4 int Apple.size N/A 28 4 java.lang.String Apple.name N/A 32 4 com.zzx.algorithm.tst.Apple Apple.brother N/A 36 4 (loss due to the next object alignment) Instance size: 40 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 1234567891011
可以看到Apple的对象头12个字节,然后分别是从Fruit类继承来的size属性(虽然Fruit的size是private的,还是会被继承,与Apple自身的size共存),还有自己定义的4个属性,基础数据类型直接分配,对象类型都是存的指针占4个字节(默认都是开启了指针压缩),最终是40个字节,所以我们new一个Apple对象,直接就会占用堆栈中40个字节的内存,清楚对象的内存分配,让我们在写代码时心中有数,应当时刻有内存优化的意识! 这里又引出了一个小知识点,上面其实已经标注出来了。
父类的私有成员变量是否会被子类继承? 答案当然是肯定的,我们上面分析的Apple类,父类Fruit有一个private类型的size成员变量,Apple自身也有一个size成员变量,它们能够共存。注意划重点了,类的成员变量的私有访问控制符private,只是编译器层面的限制,在实际内存中不论是私有的,还是公开的,都按规则存放在一起,对虚拟机来说并没有什么分别!
4、方法内部new的对象是在堆上还是栈上?
我们常规的认识是对象的分配是在堆上,栈上会有个引用指向该对象(即存储它的地址),到底是不是呢,我们来做个试验! 我们在循环内创建一亿个Apple对象,并记录循环的执行时间,前面已经算过1个Apple对象占用40个字节,总共需要4GB的空间。
public static void main(String[] args) { long startTime = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { newApple(); } System.out.println("take time:" + (System.currentTimeMillis() - startTime) + "ms"); } public static void newApple() { new Apple(); } 1234567891011
我们给JVM添加上-XX:+PrintGC运行配置,让编译器执行过程中输出GC的log日志 // 运行结果,没有输出任何gc的日志
take time:6ms 1
1亿个对象,6ms就分配完成,而且没有任何GC,显然如果对象在堆上分配的话是不可能的,其实上面的实例代码,Apple对象全部都是在栈上分配的,这里要提出一个概念指针逃逸,newApple方法中新建的对象Apple并没有在外部被使用,所以它被优化为在栈上分配,我们知道方法执行完成后该栈帧就会被清空,所以也就不会有GC。 我们可以设置虚拟机的运行参数来测试一下。 // 虚拟机关闭指针逃逸分析
-XX:-DoEscapeAnalysis 1
// 虚拟机关闭标量替换
-XX:-EliminateAllocations 1
在VM options里面添加上面二个参数,再运行一次
[GC (Allocation Failure) 236984K->440K(459776K), 0.0003751 secs] [GC (Allocation Failure) 284600K->440K(516608K), 0.0004272 secs] [GC (Allocation Failure) 341432K->440K(585216K), 0.0004835 secs] [GC (Allocation Failure) 410040K->440K(667136K), 0.0004655 secs] [GC (Allocation Failure) 491960K->440K(645632K), 0.0003837 secs] [GC (Allocation Failure) 470456K->440K(625152K), 0.0003598 secs] take time:5347ms 12345678
可以看到有很多GC的日志,而且运行的时间也比之前长了很多,因为这时候Apple对象的分配在堆上,而堆是所有线程共享的,所以分配的时候肯定有同步机制,而且触发了大量的gc,所以效率低很多。 总结一下: 虚拟机指针逃逸分析是默认开启的,对象不会逃逸的时候优先在栈上分配,否则在堆上分配。 到这里,关于“一个对象占多少内存?”这个问题,已经能回答的相当全面了。
5.看在Android ART虚拟机上面的分配情况
我们前面使用了jol工具来输出对象头的信息,但是这个jol工具只能用在hotspot虚拟机上,那我们如何在Android上面获取对象头大小呢? 可以使用sun.misc.Unsafe的objectFieldOffset方法,返回成员属性在内存中的地址相对于对象内存地址的偏移量 根据前面的知识,普通对象的结构 就是 对象头+实例数据+对齐字节,那如果我们能获取到第一个实例数据的偏移地址,其实就是获得了对象头的字节大小 5.1 如何拿到并使用Unsafe 因为Unsafe是不可见的类,而且它在初始化的时候有检查当前类的加载器,如果不是系统加载器会报错。但是好消息是,AtomicInteger中定义了一个Unsafe对象,而且是静态的,我们可以直接通过反射来得到。
public static Object getUnsafeObject() { Class clazz = AtomicInteger.class; try { Field uFiled = clazz.getDeclaredField("U"); uFiled.setAccessible(true); return uFiled.get(null); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } 12345678910111213
拿到了Unsafe,我们就可以通过调用它的objectFieldOffset静态方法来获取成员变量的内存偏移地址。
public static long getVariableOffset(Object target, String variableName) { Object unsafeObject = getUnsafeObject(); if (unsafeObject != null) { try { Method method = unsafeObject.getClass().getDeclaredMethod("objectFieldOffset", Field.class); method.setAccessible(true); Field targetFiled = target.getClass().getDeclaredField(variableName); return (long) method.invoke(unsafeObject, targetFiled); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } } return -1; } public static void printObjectOffsets(Object target) { Class targetClass = target.getClass(); Field[] fields = targetClass.getDeclaredFields(); for (Field field : fields) { String name = field.getName(); Log.e(">>>>>", name + " offset: " + getVariableOffset(target, name)); } } 12345678910111213141516171819202122232425262728
输出结果:
2019-06-25 15:57:59.176 6056-6056/com.zzx.readhub E/>>>>>: size offset: 8 2019-06-25 15:57:59.176 6056-6056/com.zzx.readhub E/>>>>>: brother offset: 12 2019-06-25 15:57:59.176 6056-6056/com.zzx.readhub E/>>>>>: create_time offset: 24 2019-06-25 15:57:59.177 6056-6056/com.zzx.readhub E/>>>>>: name offset: 16 2019-06-25 15:57:59.177 6056-6056/com.zzx.readhub E/>>>>>: size offset: 20 12345
通过输出结果,看出在 Android7.1 ART 虚拟机上,对象头的大小是8个字节,这跟hotspot虚拟机不同(hotspot是12个字节默认开启指针压缩),根据输出的结果目前只发现这一点差别,各种数据类型占用的字节数都是一样的,比如int占4个字节,指针4个字节,long8个字节等,都一样。
总结
全文我们总结了以下几个知识点
Java虚拟机通过字节码指令来操作内存,所以可以说它并不关心数据类型,它只是按指令行事,不同类型的数据有不同的字节码指令。 Java中基本数据类型和引用类型的内存分配知识,重点分析了引用类型的对象头,并介绍了JOL工具的使用 延伸到Android平台,介绍了一种获取Android中对象的对象头信息的方法,并对比了ART和Hotspot虚拟机对象头长度的差别。
-
更多推荐
所有评论(0)