美文网首页
JVM(三)堆与对象

JVM(三)堆与对象

作者: NIIIICO | 来源:发表于2022-03-30 17:48 被阅读0次

    一、堆的结构

    堆内存结构

    JVM中,堆被划分成两块区域:年轻代(young):老年代(old)= 1:2;年轻代又可以划分为Eden(伊甸园):From Survivor(幸存者):To Survivor (幸存者)= 8:1:1;以上比例都是默认比例,可以通过参数进行修改。

    为什么要分代?根本原因是为了优化GC性能。在java程序运行过程中,大部分对象都是临时对象。
    不分代,GC时需要对heap的所有区域扫描,比较消耗性能;
    分代,GC时常规情况下只处理年轻代,就可以回收大量空间,满足程序运行需要;特殊情况下再处理老年代。从而优化GC运行效率。

    二、对象的分配

    对象分配过程
    1、根据逃逸分析,判断是否可以在栈中分配

    逃逸:一个对象如果被外部其他类调用,或做用于属性中,此种现象为对象逃逸。

    • 对象被赋值给堆中对象的字段和类的静态变量。
    • 对象被传进了不确定的代码中去运行。

    非逃逸:一个对象的作用域仅限于方法内部,此种现象为非逃逸。
    逃逸分析:Hotspot虚拟机可以分析对象的使用范围,从而决定是否在Heap中分配。如某个对象没有逃逸,在虚拟机上可以做如下优化:

    (1)锁消除:我们知道同步锁比较消耗性能,由于当前对象只在一个线程中访问,可以取消同步操作。

    (2)标量替换

    • 标量是指不可分割的量,如java中基本数据类型和reference类型,相对的一个数据可以继续分解,称为聚合量;
    • 如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换;
    • 如果对象未发生逃逸,并且该对象可以被拆散,那么经过优化之后,可以不直接生成该对象,而是在栈上创建若干个成员变量;

    (3)栈上分配:如果对象未发生逃逸,该对象就可以分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,从而减少GC运行频率。

    2、大对象直接分配到Old

    Young一般都不大,如果存储大对象,Young很容易满,导致频繁触发GC。GC后很可能还是会超过Yong的存储,将对象转存到Old区,产生大内存复制。因此直接将大对象分配到Old区,减少上述操作。

    3、TLAB判断

    TLAB:Thread Local Allocation Buffer,线程本地分配缓存,TLAB在Eden区分配,默认只有Eden的1%,所以大对象无法在TLAB分配。

    • 对象一般分配在堆上,堆是线程共享的,因此在申请对象内存时需要同步操作(加锁/CAS等),使分配效率下降。
    • 在虚拟机TLAB功能启动的情况下,线程初始化时,虚拟机会为每个线程分配一块TLAB空间,TLAB只有在“分配”这个动作上是线程独占的。
    • 需要分配内存的时候,线程可以在TLAB上分配,而不需要处理多线程的情况,提高分配效率。
    4、Young、Old、GC
    • 经过上述判断后,再判断Young区域。首先判断Eden是否放的下,如果放的下,直接分配;否则触发Minor GC。
    • Minor GC会查询Eden和From Survivor区域的对象,将存活的对象放入To Survivor区域,并交换From、To。
    • 如果To Survivor放不下这些对象,会将对象转入Old区域。
    • 如果Old区域放不下,会触发Major GC/Full GC,回收Old区域的对象。

    三、对象数据

    1、对象结构
    对象结构

    在Hotspot虚拟机中,对象在内存中的结构可分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

    (1)对象头:

    • markword(4字节):存储hashcode值、GC年龄、锁状态标志、偏向线程ID、偏向时间戳等。

      markword不同状态下存储的数据
    • klass指针(4字节):对象所属类的元数据,用于确定该对象的所属类型。

    • 数组长度(4字节):如果对象是一个数组,对象头中还必须有一块数据用于记录数组长度。

    (2)实例数据:对象真正存储的数据,包含自有的字段和父类继承的。
    (3)数据填充:没有实际意义,起着占位符的作用,当实例数据没有对齐时,就需要通过对齐填充来补全。

    2、对象的创建方式
    • new:new Object();
    • 反射:Class.forName("java.lang.Object").newInstance();
    • clone:Object.clone();
    • 反序列化:ObjectInputStream.readObject();
    3、对象的创建过程

    (1)查找类元信息
    根据new的参数,在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始;如果没有,先执行相应的类加载过程。

    (2)内存分配及并发
    查找到类元信息后,虚拟机将为新生对象分配内存,对象所需的内存在类被解析加载进入元空间之后就可以计算出来:

    • 指针碰撞:如果堆区是规整的,已知对象的内存,只需要更新指针的位置即可。

      指针碰撞
    • 空闲列表:但是内存一般都是不规整的,这时候就需要维护一个空闲列表;分配对象时,先从空闲列表中寻找合适大小的内存进行分配,再从主内存中撞针分配。

      空闲列表
    • 并发:由于堆内存可以多线程访问,因此在操作时要保障线程安全,一是使用CAS(compare and swap)操作;二是上文提到的TLAB

    (3)初始化数据
    内存分配完成后,虚拟机需要将分配到的内存空间初始化为零值(不包括对象头),保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

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

    (5)执行init
    执行<init>方法,为属性赋值、执行构造方法。

    相关文章

      网友评论

          本文标题:JVM(三)堆与对象

          本文链接:https://www.haomeiwen.com/subject/xcmkjrtx.html