java 对象头_JVM 对象创建与内存分配

1、 对象的创建过程

        Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,而在虚拟机中,对象的创建会经历一系列复杂的操作,会经历类加载检查、加载类、分配内存、初始化、设置对象头、执行Init 方法等 具体流程如图:

274ff62d87b6219f8078c38fc82e95f3.png

1.1 类加载检查

        虚拟机遇到一条new指令时(new关键词、对象克隆、对象序列化等),首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

1.2 分配内存

        在类加载检查通过后,虚拟机将会为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。

内存的划分分为:

a、“指针碰撞”(Bump the Pointer)(默认用指针碰撞)

        如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

b、“空闲列表”(Free List)

        如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录。

        在内存划分的过程中,可能会出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:

一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;

一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

1.3 初始化

        内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

1.4 设置对象头

        初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。

        在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

1.5 执行方法

执行方法,为属性赋值和执行构造方法。

1.6 HotSpot解释器代码片段

// 确保常量池中存放的是已解释的类if (!constants->tag_at(index).is_unresolved_klass()) {    // 断言确保是klassOop和instanceKlassOop(这部分下一节介绍)    oop entry = (klassOop) *constants->obj_at_addr(index);    assert(entry->is_klass(), "Should be resolved klass");    klassOop k_entry = (klassOop) entry;    assert(k_entry->klass_part()->oop_is_instance(), "Should be instanceKlass");    instanceKlass* ik = (instanceKlass*) k_entry->klass_part();    // 确保对象所属类型已经经过初始化阶段    if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {        // 取对象长度        size_t obj_size = ik->size_helper();        oop result = NULL;        // 记录是否需要将对象所有字段置零值        bool need_zero = !ZeroTLAB;        // 是否在TLAB中分配对象        if (UseTLAB) {            result = (oop) THREAD->tlab().allocate(obj_size);        }        if (result == NULL) {            need_zero = true;            // 直接在eden中分配对象            retry:            HeapWord* compare_to = *Universe::heap()->top_addr();            HeapWord* new_top = compare_to + obj_size;            // cmpxchg是x86中的CAS指令,这里是一个C++方法,通过CAS方式分配空间,并发失败的            话,转到retry中重试直至成功分配为止            if (new_top <= *Universe::heap()->end_addr()) {                if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {                    goto retry;                }                result = (oop) compare_to;            }        }        if (result != NULL) {            // 如果需要,为对象初始化零值            if (need_zero ) {                HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;                obj_size -= sizeof(oopDesc) / oopSize;                if (obj_size > 0 ) {                    memset(to_zero, 0, obj_size * HeapWordSize);                }            }            // 根据是否启用偏向锁,设置对象头信息            if (UseBiasedLocking) {                result->set_mark(ik->prototype_header());            } else {                result->set_mark(markOopDesc::prototype());            }            result->set_klass_gap(0);            result->set_klass(k_entry);            // 将对象引用入栈,继续执行下一条指令            SET_STACK_OBJECT(result, 0);            UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);        }    }}

2、对象头与指针压缩

2.1 对象头信息

32对象头

c1a7c12db298936890042492167b5931.png

64位对象头

428bacac787983771d42627aca457863.png

示例代码:

import org.openjdk.jol.info.ClassLayout;public class ObjectHeaderTest {   public static void main(String[] args) {       ClassLayout objectLayout = ClassLayout.parseInstance(new Object());       System.out.println(objectLayout.toPrintable());       ClassLayout arrayLayout = ClassLayout.parseInstance(new int[]{});       System.out.println(arrayLayout.toPrintable());  }}

输出结果(-XX:+UseCompressedOops 开启指针压缩):

java.lang.Object object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE     0     4       (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)     4     4       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)     8     4       (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)    12     4       (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total[I object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE     0     4       (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)     4     4       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)     8     4       (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)    12     4       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)    16     0    int [I.                             N/AInstance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total

输出结果(-XX:-UseCompressedOops 关闭指针压缩):

java.lang.Object object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE     0     4       (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)     4     4       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)     8     4       (object header)                           00 1c c4 1b (00000000 00011100 11000100 00011011) (465837056)    12     4       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)Instance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total[I object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE     0     4       (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)     4     4       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)     8     4       (object header)                           68 0b c4 1b (01101000 00001011 11000100 00011011) (465832808)    12     4       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)    16     4       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)    20     4       (alignment/padding gap)                      24     0    int [I.                             N/AInstance size: 24 bytesSpace losses: 4 bytes internal + 0 bytes external = 4 bytes total

object header 表示对象头信息

1、 Mark Word 对象头(32位占4字节, 64位占8字节, 1 byte = 8 bit)

2、 Klass Point 类型指针(开启压缩占4字节,关闭压缩占8字节)

3、 数组长度(4字节,只有数组对象才有)

2.2 指针压缩

     UseCompressedOops 全称为compressed--压缩、oop(ordinary object pointer)--对象指针, JVM 在jdk1.6 update14开始支持指针压缩(64位操作系统)。

        在64位平台的HotSpot中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力,为了减少64位平台下内存的消耗,启用指针压缩功能。

        在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的存入堆内存时压缩编码、取出到cpu寄存器后解码方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置

指针压缩的优点:

1、减少了较大指针在主内存和缓存之间数据移动。

2、减少了内存消耗,避免了频繁调用GC

备注:

1.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间

2.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现的问题,所以堆内存不要大于32G

3、对象的内存分配

3.1 栈上分配

        通过JVM内存模型知道JAVA中的对象是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,从而减少了垃圾回收的压力。

逃逸分析:分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,那么这个对象的作用域范围不确定无法分配到栈中。当一个对象在方法中被定义后,随着方法的销毁对象就无效了,对于这样的对象我们就可以分配到栈中,当方法结束时跟随栈内存一起被回收(没有返回值的方法)。在JDK1.7以后JVM默认开始逃逸分析(-XX:+DoEscapeAnalysis)来优化对象内存的分配地址,当栈空间不足的情况下,会通过标量替换优先进行栈上分配。

标量替换:通过逃逸分析确定对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。JDK1.7 以后默认开启标量替换(-XX:+EliminateAllocations)。

示例代码:

public class EscapeAnalysisTest {   public void testEscapeAnalysis(){       Test test =new Test();       test.setParam1(1);       test.setParam2(2);  }   /**    * 打印GC信息:-XX:+PrintGC    * 使用如下参数不会发生GC    * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations    * 使用如下参数都会发生大量GC    * 关闭逃逸分析:-Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations    * 关闭标量替换:-Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations    * @param args    */   public static void main(String[] args) {       EscapeAnalysisTest test = new EscapeAnalysisTest();       while(true){           test.testEscapeAnalysis();      }  }}输出结果:[GC (Allocation Failure)  65536K->768K(251392K), 0.0006457 secs][GC (Allocation Failure)  66304K->784K(251392K), 0.0008339 secs][GC (Allocation Failure)  66320K->784K(251392K), 0.0004900 secs][GC (Allocation Failure)  66320K->688K(316928K), 0.0005758 secs]

3.2 Eden 区分配

        大多数情况下,对象会分配到新生代Eden 区中。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。Minor GC 会将垃圾对象进行回收,剩余存活的对象会被挪到为空的那块Survivor区,下一次Eden区满了后又会触发Minor GC,把Eden区和Survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的Survivor区。因为新生代的对象生命周期都很短暂,所以我们尽可能的让Eden区保证足够的大小。在JVM中通过-XX:+UseAdaptiveSizePolicy(默认开启)指令让Eden与Survivor区保持在8:1:1比例下自动变化。

ca367cf74500b0472e6a02c09eee1757.png

代码示例1:

public class EdenTest {   public static void main(String[] args) {       /**        * 打印GC详情:-XX:+PrintGCDetails        */       byte[] test = new byte[1024*1024*50];  }}输出结果:HeapPSYoungGen      total 76288K, used 57766K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000) eden space 65536K, 88% used [0x000000076b500000,0x000000076ed698b0,0x000000076f500000) from space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000) to   space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)ParOldGen       total 175104K, used 0K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000) object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1e00000,0x00000006cc900000)Metaspace       used 3228K, capacity 4496K, committed 4864K, reserved 1056768K class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

        从输出结果上看eden space 已经使用了88%,这个时候我们在分配一个同等大小的内存,当eden space 空间不够的时候会触发Minor GC,GC期间虚拟机发现Eden无法存入Survior的空间,所以只好把新生代的对象提前转移到老年代中。当老年代的空间足够则不会触发Full GC。

代码示例2:

public class EdenTest {   public static void main(String[] args) {       /**        * 打印GC详情:-XX:+PrintGCDetails        */       byte[] test = new byte[1024*1024*50];       byte[] test1 = new byte[1024*1024*50];  }}输出结果:HeapPSYoungGen      total 76288K, used 57766K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000) eden space 65536K, 88% used [0x000000076b500000,0x000000076ed698b0,0x000000076f500000) from space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000) to   space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)ParOldGen       total 175104K, used 51200K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000) object space 175104K, 29% used [0x00000006c1e00000,0x00000006c5000010,0x00000006cc900000)Metaspace       used 3228K, capacity 4496K, committed 4864K, reserved 1056768K class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

3.3 大对象直接进入老年代

        JVM可以通过参数 -XX:PretenureSizeThreshold 或者-XX:+UseSerialGC设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,从而避免为大对象分配内存时的复制操作而降低效率。(参数只在Serial 和ParNew两个收集器中有效)

代码示例:

public class EdenTest {   public static void main(String[] args) {       /**        * 打印GC详情:-XX:+PrintGCDetails        * -XX:+PrintGCDetails -XX:PretenureSizeThreshold=100000        */       byte[] test = new byte[100 * 1024 * 1024];  }}

输出结果:

HeapPSYoungGen      total 76288K, used 6566K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000) eden space 65536K, 10% used [0x000000076b500000,0x000000076bb698a0,0x000000076f500000) from space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000) to   space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)ParOldGen       total 175104K, used 102400K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000) object space 175104K, 58% used [0x00000006c1e00000,0x00000006c8200010,0x00000006cc900000)Metaspace       used 3229K, capacity 4496K, committed 4864K, reserved 1056768K class space    used 350K, capacity 388K, committed 512K, reserved 1048576

3.4 长期存活的对象将进入老年代

        JVM采用了分代收集的思想来管理内存,他给每个对象分配一个年龄(Age)计数器。如果对象在Eden 出生并经过第一次Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在 Survivor 中每经历一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15岁,不同的垃圾收集器会略微有点不同),就会被转移到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold 来设置。

3.5 对象动态年龄判断

      当Minor GC触发后,如果这一批对象的总大小大于这块Survivor区域中(S0/S1 用于存放对象的s区)内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了S0区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。

3.6 老年代空间分配担保机制

        在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次Minor GC是安全的。如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

为什么要进行空间担保?

        是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值