Java对象创建和内存分配

桃扇骨 2024-03-23 15:30 114阅读 0赞

Java对象创建流程如下步骤

在这里插入图片描述
判断是否加载类
当Java虚拟机执行一条new指令时,首先会检查这个指令的参数是否能在常量池中定位到类的符号引用,并且检查该类是否被加载、验证、准备、解析和初始化过。如果没有则执行加载过程。

给对象分配内存
对象所需的大小在类加载完成后便可以确定,为对象分配内存实际上就是把等同于一个确定大小的内存空间从Java堆中分配出来。

  • 分配内存的两中方式

    • 指针碰撞(Bump the pointer)
      Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器没有产生内存碎片,所以采用的是”指针碰撞”。
    • 空闲列表(Free List)
      Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于”标记-清除”的收集器,会产生内存碎片,空闲内存跟已经分配的内存相互交错,所以采用的是”空闲列表”。
  • 并发处理
    对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:

    • 虚拟机采用CAS加上失败重试的方式保证更新操作的原子性
    • 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB) :把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。可用JVM参数-XX:+/-UseTLAB参数设定。

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

  • 注意:类的成员变量可以不显示地初始化(Java虚拟机都会先自动给它初始化为默认值)。方法中的局部变量如果只负责接收一个表达式的值,可以不初始化,但是参与运算和直接输出等其它情况的局部变量需要初始化。

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

执行init()
在上面的工作都完成之后,从虚拟机的角度看,一个新的对象已经产生了。但是从Java程序的角度看,对象的创建才刚刚开始init()方法还没有执行,所有的字段都还是零。
所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行init()方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。

对象内存分配

基本原则
对象优先在Eden区进行分配
大对象直接进入老年代
长期存活的对象将进入老年代

动态对象年龄判定
HotSpot虚拟机中如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间分配担保
在发生Minor GC之前, 虚拟机必须要检查老年代最大可用的连续空间是否大于新生代所有对象的空间,如果大于, 则此次Minor GC是安全的。如果不大于,则虚拟机会首先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败;如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试一次Minor GC;如果小于,或者-XX:HandlePromotionFailure 设置为不允许冒险,那么就会进行一次Full GC。

启动空间分配担保机制是有一定风险的。如果此次是Full GC并启用了空间分配担保机制,因为不清楚将要进入老年代对象的大小,仅能通过以往Full GC 进行评估, 有可能出现一种情况,实际上进入老年代的对象大小要大于老年的可用空间(极端情况是上次内存回收时新生代中所有对象都存活),这样就会出现内存溢出。

注意: 在JDK6 Update24之前,-XX:HandlePromotionFailure需要用户自己设置,之后,虽然虚拟机仍有这个参数,但实际上虚拟机不管有没有设置这个值,都会执行相对的规则:只要老年代的连续空间大于新生代对象总大小或历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。

逃逸分析(栈内分配)

  • 逃逸分析
    分析对象动态作用域,当一个对象在对象里面被定义后,如果被外部方法引用(列入传参),这是方法逃逸;如果本线程的对象被其它线程访问到,则是线程逃逸。从不逃逸、方法逃逸到线程逃逸被称为对象由低到高的不同逃逸程度。
  • 栈上分配
    一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象便可以使用栈上分配,通过随着方法的结束销毁大量的对象可以降低垃圾收集子系统的压力。栈上分配可以支持方法分配,但不支持线程逃逸。
  • 标量替换
    标量: 若一个数据已经无法再分成更小的数据来表示(例如原始数据类型:int,long, reference等),那么这些数据被称为标量。
    聚合量: 如果一个数据可以继续分解,那么这个数据被称为聚合量。
    标量替换: 如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量回复为原始类型来访问,整个过程被称为标量替换。

如果逃逸分析证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不创建这个对象,而是改为直接创建它的若干各被这个方法使用的成员变量来代替。标量替换可以看成栈上分配的一种特例,实现简单,但对逃逸分析要求高,它不允许对象逃逸出方法范围。

命令
开启逃逸分析:-XX:+DoEscapeAnalysis
查看逃逸分析的分析结果: -XX:+PrintEscapeAnalysis
开启标量替换:-XX:+EliminateAllocations
查看标量替换情况: -XX:+PrintEliminateAllocations

对象内存分配流程图
在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,114人围观)

还没有评论,来说两句吧...

相关阅读