深入理解JVM(1) Java内存模型

内存分块

在这里插入图片描述
两个区分点:【运行时数据区与代码区】、【线程共享数据区与线程隔离数据区
heap是共享的,VM stack是线程隔离的

私有内容

  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地
    址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)
  • VM stack的生命周期与线程相同。VM stack有局部变量表,记录局部变量、引用和返回地址,double和long(它们的长度为64bit)占2个slot,其余占1个,但slot则是不定长的。
  • HotSpot虚拟机不支持动态扩展,所以线程请求栈失败时(请求深度大于允许深度),就必然报OOM错误。
  • 本地方法栈是为本地方法开辟的栈,相当与线程的“跑腿小弟”,所以它们应当也是线程私有的。HotSpot中本地方法栈和VM stack合二为一。

线程共享内容

老版本:所有的对象实例以及数组都应当在堆上分配
现在有栈上分配、标量替换等手段,但总体上绝大多数对象分配在堆上
经典GC采用分分代收集方式回收内存

  • 主流JVM的堆是可扩展的,通过参数-Xmx(最小)和-Xms(最大)设定(这两个参数的值并非固定,而是受运行时的内存占用而改变),因此仅当堆内存分配失败,且扩展也失败的时候,Java虚拟机才会报OOM异常。

方法区

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,相当于上层代码区。
Java8之后的方法区使用本地内存(native memory)实现,在无法满足新内存分配时OOM

  • 运行时常量池(runtime constant pool)是方法区的一部分,它基于class文件中的常量池表,后者存放编译器生成的字面量和符号引用,并在类加载后放在常量池中。

直接内存

不是虚拟机运行时数据的一部分,是实现NIO的一种机制,NIO基于通道Channel和缓冲区(buffer)实现,使用native函数库直接分配堆外内存并进行操作,进而避免在Java堆和native堆中来回复制数据。

HotSpot在Java堆上的内存使用

对象的创建过程

此处讨论的对象是普通对象,不包括数组和Class对象

  1. 当字节码第一次遇到new指令时,首先检查指令参数(及类符号)是否能在常量池中定位到一个符号引用,且检查该类是否被加载、解析和初始化过,如果没有首先要进行类加载。这个类加载过程包括对类的静态对象引用的初始化和类静态初始化块的执行。
  2. 之后虚拟机为新生对象分配内存,对象大小在类加载时后完全决定,在堆内存中取出一块对应大小的区域进行分配。另外,对象的创建不是线程安全的,虚拟机有两种选择(1)采用CAS+失败重试的方法保证更新操作的原子性(2)每个线程预先分配一块内存,将空间分割(TLAB),虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。可以看出第一种方法简单但耗时,后一种方法复杂但用空间换时间,适合对象密集创建的场景。

空闲内存的分配方式与GC算法有关,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于标记-清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存
强调“理论上”是因为在CMS的实现里面,为了能在多数情况下分配得更快,设计了一个叫作Linear Allocation Buffer的分配缓冲区,通过空闲列表拿到一大块分配缓冲区之后,在它里面仍然可以使用指针碰撞方式来分配。

  1. 完成内存分配后,要对对象赋默认零值,标准是新创建的实例可以直接使用。
  2. 之后JVM对对象进行必要的设置,确定是哪个类的实例,找到metadata的方法,hashcode的计算,GC分代年龄等,这些信息存在Object Header中。过程后,才会开始执行程序制定的构造函数
  3. 完成配置后,执行构造函数之前,类生命的非静态成员变量的初始化和初始化块代码
  4. 上述操作后,再执行指定的构造函数

对象的内存模型

HotSpot中的对象内存模型分三块

  • 对象头
  • 实例数据(Instance data)
  • 填充(padding),将对象的大小填充为8byte的整数倍

对象头

  • 第一类是存储对象自身的运行时数据,包括但不限于:
    hashcode计算、GC分代年龄、锁状态标志、线程持有的锁、Biased线程ID、Biased时间戳等,它们被放在一起用动态(可以压缩空间)Bitmap存储,称为MarkWord

  • 第二类是类型指针,几对象指向它的类型元数据的指针,JVM通过它来判定该对象是哪个类的实例

实例数据

在这里插入图片描述

在这里插入图片描述
是对象真正存储的有效信息,从父类继承下来的,还是在子类中定义的字段都必须记录。其存储策略受参数-XX:FieldsAllocationStyle和字段定义顺序的影响。
HotSpot默认分配顺序为为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),相同宽度的字段被分配到一起存放,父类在子类前存放,如果+XX:CompactFields参数值为true(也是默认值),子类之中的窄变量允许插入父类变量的空隙中以节省空间。

对象的访问定位

主流的访问方式:句柄或直接指针

  • 句柄访问时,Java堆划分出一块内存作为句柄池,引用存储句柄,举并包含对象实例数据与类型数据各自具体的地址信息:
    在这里插入图片描述
  • 直接地址访问时,引用就是地址,访问实例数据可以少一次间接开销
    在这里插入图片描述

句柄访问的优势是在对象移动时,只需要修改句柄中的示例数据指针,而不需要修改reference,而直接访问地址可以少一次内存访问。

HotSpot主要使用直接访问。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值