JVM-对象的内存模型

对象内存模型

一个Java对象在内存中包括3个部分:对象头,实例数据,对其填充
在这里插入图片描述
32位处理器,一次能够去处理32个二进制位,4字节的数据
64位处理器,一次能够去处理64个二进制位,8字节的数据
指针压缩技术
JDK1.6出现的,开启了指针压缩,什么时候指针压缩会失效?
超过32G指针压缩无效

在这里插入图片描述
**小端存储:**便于数据之前的数据转换,例如long类型转换为int类型,高地址部分的数据可以直接截掉
大端存储: 便于数据类型的符号判断,因为最低地址位数据即为符号位,可以直接判断数据的正负号。
java中使用的是大端存储

内存模式设计–Class Pointer

句柄池访问:
在这里插入图片描述
直接指针访问:
在这里插入图片描述
区别:
句柄池:
使用句柄池访问对象,会在堆中开辟一块内存作为句柄池,句柄中存储了对象实例数据(属性值结构体)的内存地址,访问类型数据的内存地址(类信息,方法类型信息),对象实例数据一般也在heap中开辟,类型数据一般存储在方法区中。
优点: reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象时非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变
缺点: 增加了一次指针定位的时间开销

直接访问:
直接指针访问指reference中直接存储对象在heap中的内存地址,但对应的类型数据访问地址需要在实例中存储
优点: 节省了一次指针定位的开销
缺点: 在对象被移动时(如进行GC后的内存重新排列),reference本身需要被修改

内存模型设计—指针压缩

指针压缩的目的:

  • 为了保证CPU普通对象指针缓存
  • 为了减少GC的发生,因为指针不压缩时8字节,这样在64为操作系统的堆上其他资源空间就少了

64位操作系统中,内存 > 4G默认开启指针压缩技术,内存<4G. 默认32位系统不开启,内存 > 32G指针压缩失效,所以我们通常在部署服务时,JVM内存不要超过32G,因为超过32G就无法开启指针压缩了

内存 > 32G指针压缩失效的原因时:4G 8 = 32G
32位系统的CPU最大支持2^ 32=4G,如果是64位系统,最大支持2^32=4G但是对其填充是按照8字节进行填充,指针压缩可以理解位32位系统在64位上使用,因为32位系统的CPU寻址空间最大支持4G,对其填充8 = 32G,这就是内存>32G指针压缩失效的原因

关闭指针压缩: -XX:UseCompressedOops

内存模型设计—对齐填充

对齐填充的意义是提高CPU访问数据的效率,主要针对会存在该实例对象数据跨内存地址区域存储的情况
例如:在没有对齐填充的情况下,内存地址存放情况如下
在这里插入图片描述
因为处理器只能0x00 - 0x07,0x08 - 0x0F这样读取数据,所以当我们想获取这个long类型的数据时,处理器必须要读两次内存,第一次(0x00 - 0x07),第二次(0x08 - 0x0F),然后将两次的结果才能获得真正的数据

那么在有对齐填充的情况下,内存地址存放情况是这样的
在这里插入图片描述
现在处理器只需要直接读取(0x08-0x0F)的内存地址就可以获得我们想要的数据了
当我们的策略为0时,这个时候我们的排序时: 基本类型 > 填充字符 > 引用类型
当我们的策略为1时,引用类型 > 基本类型 > 填充字段
当我们的策略为2时,父类中的引用类型跟子类中的引用类型放在一起,父类采用策略0,子类采用策略1
这样操作可以降低空间的开销

JVM内存模型

运行时数据区

上面对运行时数据区描述了很多,其实重点存储数据的时堆和方法区,所以内存的设计也着重从这两方面展开(注意这两块区域都是线程共享的)

对于虚拟机栈,本地方法栈,程序计数器都是线程私有的
可以这样理解,JVM运行时数据区是一种规范,而JVM内存模型是对该规范的实现

图形展示

一块时非堆区,一块是堆区
堆区分为两大块:一个时old区,一个时young区
young区分为两大块,一个Survivor区(S0 + S1),一块时Eden区
S0和S1一样大,也可叫From和To

在这里插入图片描述
在这里插入图片描述

对象创建过程

一般情况下,新创建的都会分配到Eden区,一些特殊的大对象会直接分配到Old区

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的。

在这里插入图片描述

什么时候会触发FUll GC?

  • 之前每次晋升的对象的平均大小 > 老年代的剩余空间 ---- 基于历史平均水平
  • young GC之后,存活对象超过了老年代的剩余空间 ------基于下一次可能的剩余空间
  • Meta Space区域空间不足
  • System.gc()

方法区,类信息,静态变量,常量,即时编译之后的代码,运行时常量池

JDK1.7之前,Perm Space 永久代,JVM自己的内存,线性整理,会增加垃圾回收的时间
JDK1.8 Meta Space 元空间,直接内存,减少内存碎片,节省压缩时间

常见问题

  • 如何理解minor GC / Major GC / Full GC

Minor GC: 新生代
Major GC:老年代
Full GC:新生代 + 老年代

  • 为什么需要Survicor区? 只有Eden不行吗?

如果没有Survicor,Eden没进行一次Minor GC,存活的对象就会被送到老年代,这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看作触发了Full GC)
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长的多
执行时间长有什么坏处? 频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度
可能你会说,那就对老年代的空间进行增加或者减少
假如增加老年代的空间,更多存活对象才能填满老年代,虽然降低full GC频率,但是随着老年代空间加大,一旦发生Full GC执行所需要的时间更长
假如减少老年代的空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16Minor GC还能在新生代存活的对象,才会被送到老年代

  • 为什么需要两个Survivor区

最大的好处时解决了碎片化,也就是为什么一个Survivor区不行?第一部分中,我们知道必须设置Survivor区,假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在·Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活就会被移动到Survivor区,这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Suvivor各有一些存活对象,如果此时把Eden的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存时不连续的,也就导致了内存碎片化
永远有一个Survivor space时空的,另一个非空的Survivor space五碎片

  • 新生代中Eden:S1:S2为什么时8:1:1?

新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1为8:1
即新生代中Eden:s1:s2为8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IMB公司的专门研究,新生代中的对象大概98%都是"朝生夕死"的

  • 堆内存中都是线程共享的区域吗?

JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,全称为:ThreadLocal Allocation Buffer
对象有限会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值