一道面试题。 问题是: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对象占用内存是如何分配的。
一个Java对象在内存中有三部分组成:1,对象头。2,实例数据。3,内存填充。
看下面一张图:
32位的对象头
64位的对象头
小结:
上面了解了对象头的组成和大小信息。下面就了解下对象的大小和内存布局。
先看一个例子:
/** * @author jiexiu * created 2019/12/14 - 10:15 */ public class EmptyObject { } EmptyObject emptyObject = new EmptyObject();
emptyObject
大小是多大呢?
答案和分析:没有任何字段,父类是Object类,也没有任何字段。那么它的大小在64JVM开启指针压缩的情况下是 8 + 4 = 12 字节。 理论上没有问题,但是实际上JVM为了内存对齐,还有4字节的填充,所以总的大小是16字节。
测试代码:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency> 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
观察数据总结结论:
测试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也分配了内存。
答案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日志输出也不是如此。
结论: