Java 对象内存布局
一道面试题。 问题是:A和B两个类,A类中有一个private的字段age,B类继承自A类。创建一个B类的对象b,对象b的内存中是否包含父类A中的字段age的内存空间。
类似代码如下:
/** * @author jiexiu * created 2019/12/14 - 09:26 */ public class Animal { private int age; public int getAge() { return age; } } /** * @author jiexiu * created 2019/12/14 - 09:26 */ public class Dog extends Animal { private double weight; }
这个问题刚开始你一听可能觉得很懵,父类的private字段子类也不能 直接 访问,那么在子类对象的内存空间中还有分配的必要吗?但是你静下心来推敲一下,答案是不言自明的。可以通过反正法来证明。
- 通过Java反射API,子类中也是可以访问到父类中声明的private字段的。如果子类对象的内存中没有这部分内存内容,那么JVM该从哪里去找呢?不能无中生有的。
- 父类的字段的是private的,但是父类中提供了public方法来访问声明的private字段。
问题回答完了。但是该问题的引深问题是Java对象占用内存是如何分配的。
Java对象内存布局
一个Java对象在内存中有三部分组成:1,对象头。2,实例数据。3,内存填充。
看下面一张图:
对象头
- Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。在32位系统占4字节,在64位系统中占8字节;
- Class Pointer:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节(如果在64位JVM上开启了压缩指针,那么占用4个字节);
- Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;
32位的对象头
64位的对象头
小结:
- 32 位JVM上,非数组对象头占用8个字节,数组对象头占用12个字节。
- 64 为JVM上,非数组对象头占用16个字节,数组对象占用20个字节。如果开启压缩指针的话,非数组对象头占用12个字节(8 + 4),数组对象头占用16个字节(8 + 4 + 4)。
对象大小
上面了解了对象头的组成和大小信息。下面就了解下对象的大小和内存布局。
先看一个例子:
/** * @author jiexiu * created 2019/12/14 - 10:15 */ public class EmptyObject { } EmptyObject emptyObject = new EmptyObject();
emptyObject
大小是多大呢?
答案和分析:没有任何字段,父类是Object类,也没有任何字段。那么它的大小在64JVM开启指针压缩的情况下是 8 + 4 = 12 字节。 理论上没有问题,但是实际上JVM为了内存对齐,还有4字节的填充,所以总的大小是16字节。
测试代码:
org.openjdk.jol jol-core 0.9 System.out.println(ClassLayout.parseClass(EmptyObject.class).toPrintable());
输出结果:
com.leokongwq.java.jvm.EmptyObject 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
使用文章开头定义的 Animal
类再次试验,输出如下:
com.leokongwq.java.jvm.Animal object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Animal.age N/A Instance size: 16 bytes
因为 age是int类型,占用4个字节。和对象头加起来刚好是8字节的整数倍,不需要填充。
如果我们把age的类型定义为long,输出结果如下:
com.leokongwq.java.jvm.Animal object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (alignment/padding gap) 16 8 long Animal.age N/A Instance size: 24 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
结果表明还是有4字节的填充。
在 age
字段后面再定义一个4字节的字段,再次测试。
public class Animal { private long age; private float weight; public long getAge() { return age; } }
输出结果如下:
com.leokongwq.java.jvm.Animal object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 float Animal.weight N/A 16 8 long Animal.age N/A Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
从这次结果可以看出来,JVM会对字段进行排序,尽可能的利用内存,减少padding。
一个复杂的内存布局
/** * @author jiexiu * created 2019/12/14 - 09:26 */ public class Animal { private int height; private int age; public long getAge() { return age; } } /** * @author jiexiu * created 2019/12/14 - 09:26 */ public class Dog extends Animal { private int f1; private char f2; private double weight; private boolean f3; private Object object; private byte f4; }
运行测试代码,结果如下:
com.leokongwq.java.jvm.Dog object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Animal.height N/A 16 4 int Animal.age N/A 20 4 int Dog.f1 N/A 24 8 double Dog.weight N/A 32 2 char Dog.f2 N/A 34 1 boolean Dog.f3 N/A 35 1 byte Dog.f4 N/A 36 4 java.lang.Object Dog.object N/A Instance size: 40 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
观察数据总结结论:
- 任何对象的大小都是8个字节的整数倍,不够的进行padding。目的是为了提高CPU的工作效率。
- 实例域按照如下优先级进行排列:长整型和双精度类型;整型和浮点型;字符和短整型;字节类型和布尔类型,最后是引用类型。这些实例域都按照各自的单位对齐。目的是为了减少padding。
- 不同类继承关系中的实例域不能混合排列。首先按照规则2处理父类中的实例域,接着才是子类的实例域
- 当父类中最后一个成员和子类第一个成员的间隔如果不够4个字节的话,就必须扩展到4个字节的基本单位。
- 如果子类第一个实例域是一个双精度或者长整型,并且父类并没有用完8个字节,JVM会破坏规则2,按照整形(int),短整型(short),字节型(byte),引用类型(reference)的顺序,向未填满的空间填充。
回到面试题
测试Dog类的内存布局,结果如下:
com.leokongwq.java.jvm.Dog object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Animal.age N/A 16 8 double Dog.weight N/A Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
结果一目了然,父类Animal的private字段age也分配了内存。
题外话-Java对象全部分配在堆上吗?
答案1:全部分配在堆上。没问题,可以这么设计,但是需要考虑分配效率问题和GC压力。
答案2:也可能分配在栈上。当开启逃逸分析 -XX:+DoEscapeAnalysis
时,方法内部分配的对象完全可以在栈上分配,只要对象引用不会逸出到方法外面。方法调用结束,内存释放,GC压力也没有了。
做个试验:
-Xmx10M -XX:+PrintGCDetails -XX:+DoEscapeAnalysis /** * @author jiexiu * created 2019/12/14 - 09:26 */ public class Dog extends Animal { // private byte[] _M = new byte[1024 * 1024]; private int f1; private char f2; private double weight; private boolean f3; private Object object; private byte f4; } public static void main(String[] args) { System.out.println(ClassLayout.parseClass(Dog.class).toPrintable()); System.out.println("********************"); long startTime = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { new Dog(); } System.out.println("take time:" + (System.currentTimeMillis() - startTime) + "ms"); }
输出如下:
com.leokongwq.java.jvm.Dog object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Animal.height N/A 16 4 int Animal.age N/A 20 4 int Dog.f1 N/A 24 8 double Dog.weight N/A 32 2 char Dog.f2 N/A 34 1 boolean Dog.f3 N/A 35 1 byte Dog.f4 N/A 36 4 java.lang.Object Dog.object N/A Instance size: 40 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total ******************** [GC (Allocation Failure) [PSYoungGen: 1631K->512K(2048K)] 3217K->2297K(9216K), 0.0006654 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1535K->224K(2048K)] 3321K->2209K(9216K), 0.0012281 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1247K->0K(2048K)] 3233K->2193K(9216K), 0.0004912 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1023K->96K(2048K)] 3217K->2289K(9216K), 0.0003646 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1119K->0K(2048K)] 3313K->2217K(9216K), 0.0009835 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1024K->0K(2048K)] 3241K->2217K(9216K), 0.0004940 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1024K->0K(2048K)] 3241K->2217K(9216K), 0.0003785 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1024K->0K(2048K)] 3241K->2217K(9216K), 0.0003809 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1024K->0K(2048K)] 3241K->2217K(9216K), 0.0003755 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1024K->0K(2048K)] 3241K->2217K(9216K), 0.0003896 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [PSYoungGen: 1024K->0K(2048K)] 3241K->2217K(9216K), 0.0003721 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1024K->0K(2048K)] 3241K->2217K(9216K), 0.0003373 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1024K->0K(2048K)] 3241K->2217K(9216K), 0.0003653 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] take time:13ms Heap PSYoungGen total 2048K, used 350K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000) eden space 1024K, 34% used [0x00000007bfd00000,0x00000007bfd57930,0x00000007bfe00000) from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000) to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000) ParOldGen total 7168K, used 2217K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000) object space 7168K, 30% used [0x00000007bf600000,0x00000007bf82a460,0x00000007bfd00000) Metaspace used 8039K, capacity 8280K, committed 8576K, reserved 1056768K class space used 950K, capacity 1032K, committed 1152K, reserved 1048576K
耗时: 13毫秒,分配了 40byte * 100000000 ≈ 4G。 很明显如果是在10M的堆上进行分配,不能够这么快,GC日志输出也不是如此。
结论:
- 不会溢出的对象完全没有再堆上分配的必要,可以减少内存分配的竞争,减轻GC的压力。
- 不是所有的对象都在栈上分配。因为栈的大小是有限的,例如1M的栈大小怎么能分配2M大小的对象呢?
- 如果把Dog类中的1M大小的字节数组字段打开注释,重新测试,内存分配就没有这么快了。