JVM(三)堆与对象

一、堆的结构

堆内存结构

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
执行方法,为属性赋值、执行构造方法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值