在面试官面前侃侃而谈之JVM内存分配策略

4 篇文章 0 订阅

高级的人设,却是普通的人生。

JVM在面试中的考察频率不多说了吧,而面对面试官一个又一个埋的坑怎么才能做到侃侃而谈。对于任何知识点,先抓主干,再摸细节,在主干的理解下深入细节,方能大彻大悟。

说到对象的内存分配,大的层面来说,就是在堆中给对象分配内存,不过也并不是所有的对象和数组都在堆上分配内存空间,随着JIT编译器的发展,在编译期间,,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。

逃逸分析:
目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

栈上分配:
栈上分配就是把⽅法中的变量和对象分配到栈上,⽅法执⾏完后⾃动销毁,⽽不需要垃圾回收的介⼊,从⽽提⾼系统性能。

-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启,其它版本未测试)
-XX:-DoEscapeAnalysis 关闭逃逸分析

这里不细说JIT编译器的逃逸优化。我们继续堆内存分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下可能会直接分配在老年代中。

堆内存分配

分配规则:

主要分配在Eden区

对象主要分配在新⽣代的 Eden 区上,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。但也有一种情况,在内存担保机制下,无法安置的对象会直接进到老年代。

大对象直接分配到老年代

大对象是指需要大量连续内存空间的对象,比如很大的字符串或者数组。

主要的原因就是如果放到新生代的话,假如这个对象存活时间挺久的,那每一次MinorGC就会存在对象在Eden区及两个Survivor区之间发生大量的内存复制,造成大量的性能损耗。而且造成新生代可用空间严重不足,然后频繁GC,也会造成性能损耗。

虚拟机提供了一个参数,令大于这个设置值的对象直接在老年代分配。

-XX:PretenureSizeThreshold

注意: 这个参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge 收集器一般并不需要设置,如果遇到必须使用这个参数的场合,可以考虑ParNew加CMS的收集器的组合。

长期存活的对象将进入老年代

虚拟机采用分代的收集思想来管理内存,每次对象回收时就必须能识别哪些对象应放在新生代,哪些对象应该放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1 。对象在Survivor区中每经过一次MinorGC,年龄就加1岁,当年龄达到15岁(默认值),就会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数设置。

-XX:MaxTenuringThreshold

在这里插入图片描述

动态年龄判断

为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求兑现过的年龄必须达到了MaxTenuringThreshold才能晋升老年代。

除了对象的年龄达到了MaxTenuringThreshold(默认15)能晋升老年代。还有一个条件:

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

举个小栗子,如对象年龄5的占34%,年龄6的占36%,年龄7的占30%,按照对象年龄进行区别,对象是不能进入老年代的,但Survivor都已经100%了啊?

大家可以关注这个参数TargetSurvivorRatio,目标存活率,默认为50%。大致意思就是说年龄从小到大累加,如加入某个年龄段(如栗子中的年龄6)后,总占用超过Survivor空间TargetSurvivorRatio的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即栗子中的年龄6,7对象)。动态对象年龄判断,主要是被TargetSurvivorRatio这个参数来控制。而且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小。
在这里插入图片描述

老年代分配担保

新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。

空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间

  • 如果大于,则此次Minor GC是安全的
  • 如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。

如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

上面提到了Minor GC依然会有风险,是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。

取平均值仍然是一种概率性的事件,如果某次Minor GC后存活对象陡增,远高于平均值的话,必然导致担保失败,如果出现了分配担保失败,就只能在失败后重新发起一次Full GC。虽然存在发生这种情况的概率,但大部分时候都是能够成功分配担保的,这样就避免了过于频繁执行Full GC。

分配担保主要是在minor gc发生之前判断一下后续是否会发生full gc,如果后续大概率发生的话,则先执行full gc,再执行minor gc会更快,如果不这样优化的话,前期minor gc和后续 full gc都将会消耗比较多的时间。

提出问题,JVM的分代年龄为什么是15?而不是其他数字

我们看看对象头布局

在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0
在这里插入图片描述

在 32 位系统下,存放 Class 指针的空间大小是 4 字节,Mark Word 空间大小也是4字节,因此头部就是 8 字节,如果是数组就需要再加 4 字节表示数组的长度在这里插入图片描述
在 64 位系统及 64 位 JVM 下,开启指针压缩,那么头部存放 Class 指针的空间大小还是4字节,而 Mark Word 区域会变大,变成 8字节,也就是头部最少为12字节。

结论呢?

毫无疑问,不管在32位还是64位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的空间中,都是4bit用于存储对象分代年龄。

明白是什么原因了吗?对象的分代年龄占4位,也就是0000,最大值为1111也就是最大为15,而不可以是其他的值了。

文章持续更新,可以微信搜索「 绅堂Style 」第一时间阅读,回复【资料】有我准备的面试题笔记。
GitHub https://github.com/dtt11111/Nodes 有总结面试完整考点、资料以及我的系列文章。欢迎Star。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值