JAVA 对象布局和对象逃逸

linux在上有用户空间和内核空间的概念,进程在linux上运行会在用户空间分配内存,jvm是在用户空间申请的内存后,我们在java程序上创建对象就不用再通过系统调用去申请内存了.减少系统调用,而是使用已经分配好的内存去使用,在这记录下java创建对象的对象布局和对象逃逸

一、JAVA 对象布局
在 HotSpot虚拟机中,对象在内存中的存储的布局可以分为三块区域:对象头(Header),字段数据(field Data)和对齐填充(Padding)
在这里插入图片描述

1. Object Header(对象头):

Mark Word(标记字段)(这里是64位占8个字节,如果是32位对象头就占4个字节)—— 比如对象锁,锁状态,哈希值,GC分代年龄,当前线程指针,偏向锁的时间戳(Epoch)。细节点:图中可以看出GC分代年龄为4bit,这也是为什么minor GC(young GC)默认为15岁的原因。

Klass Pointer(类型指针)(为了节省空间64位的JVM默认开启指针压缩占4个字节,不开启压缩占8个字节)—— 就是指向方法区中的类元数据的指针,这样该对象可随时知道自己是哪个Class的实例。

数组长度 注意点:这部分只有数组对象才有,同时在64位JVM中原本也是占8个字节,但是默认会开启指针压缩,所以只会占4个字节

2. Object Body(对象体):

实例数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。也就是说,除去静态变量和常量值放在方法区,非静态变量的值是随着对象存储在堆中的。
因为修改静态变量会反映到方法区中class的数据结构中,故而推测对象保存的是静态变量和常量的引用

3. Padding(字节对齐):

用于确保对象的总长度为8字节的整数倍。
HotSpot要求对象的总长度必须是8字节的整数倍。由于对象头一定是8字节的整数倍,但实例数据部分的长度是任意的。因此需要对齐补充字段确保整个对象的总长度为8的整数倍。

public class Penghui extends People {

    private int sex;

    private int[] c;

    public static void main(String[] args) {

        ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());

        ClassLayout layout2 = ClassLayout.parseInstance(new Object());
        System.out.println(layout2.toPrintable());

        ClassLayout layout3 = ClassLayout.parseInstance(new Penghui());
        System.out.println(layout3.toPrintable());

//        byte a = 1;
//        ClassLayout layout4 = ClassLayout.parseInstance(a);
//        System.out.println(layout4.toPrintable());


    }
}

在这里插入图片描述

  1. 所以可以得出一个对象如果是普通对象的话对象头12byte, 如果是数组对象对象头16byte,也就是说对象头最少12byte
  2. 当new Object();时, 对象头12byte, 还没有对象体,会填充4byte external ,刚好16byte
  3. 一个对象都要是8byte的倍数,不够就填充

二、对象逃逸
我们常规的认识是对象的分配是在堆上,栈上会有个引用指向该对象(即存储它的地址),到底是不是呢,我们来做个试验!
我们在循环内创建10亿个Penghui对象,并记录循环的执行时间,前面已经算过1个Penghui对象占用24个字节,总共需要10亿*24byte的空间。

public static void main(String[] args) {
     long startTime = System.currentTimeMillis();
     for (int i = 0; i < 1000000000; i++) {
         newPenghui();
     }
     System.out.println("take time:" + (System.currentTimeMillis() - startTime) + "ms");
}

public static void newPenghui() {
     new Penghui();
}

我们给JVM添加上-XX:+PrintGC运行配置,让编译器执行过程中输出GC的log日志
// 运行结果,没有输出任何gc的日志

take time:38ms

10亿个对象,38ms就分配完成,而且没有任何GC,显然如果对象在堆上分配的话是不可能的,其实上面的实例代码,Penghui对象全部都是在栈上分配的,这里要提出一个概念指针逃逸,newPenghui方法中新建的对象Penghui并没有在外部被使用,所以它被优化为在栈上分配,我们知道方法执行完成后该栈帧就会被清空,所以也就不会有GC。
我们可以设置虚拟机的运行参数来测试一下。

// 虚拟机关闭指针逃逸分析
-XX:-DoEscapeAnalysis
// 虚拟机关闭标量替换
-XX:-EliminateAllocations

[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:52780ms

可以看到有很多GC的日志,而且运行的时间也比之前长了很多,因为这时候Penghui对象的分配在堆上,而堆是所有线程共享的,所以分配的时候肯定有同步机制,而且触发了大量的gc,所以效率低很多。
总结一下: 虚拟机指针逃逸分析是默认开启的,对象不会逃逸的时候优先在栈上分配,否则在堆上分配。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值