🏆 【JVM 性能调优】「对象内存分配」虚拟机参数调优分析
内容简介
本文主要针对于综合层面上进行分析 JVM 优化方案总结和列举调优参数计划。主要包含:
调优之逃逸分析(栈上分配)
调优之线程局部缓存(TLAB)
调优之 G1 回收器
栈上分配与逃逸分析
-XX:+DoEscapeAnalysis
逃逸分析(Escape Analysis)
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸。
方法逃逸的几种方式如下:
栈上分配
栈上分配是 Java 虚拟机提供的一种优化技术
基本思想
"对于那些线程私有的对象(指的是不可能被其他线程访问的对象),可以将它们直接分配在栈上,而不是分配在堆上"。
分配在栈上的好处:可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,减轻 GC 压力,从而提升系统的性能。
使用场景
线程私有对象
受虚拟机栈空间的约束,适用小对象,大对象无法触发虚拟机栈上分配。
线程私有变量,大对象虚拟机会分配到 TLAB 中,TLAB(Thread Local Allocation Buffer)
在栈上分配该对象的内存,当栈帧从 Java 虚拟机栈中弹出,就自动销毁这个对象。减小垃圾回收器压力。
虚拟机内存逻辑图
JVM 内存分配源码:
new 关键字直接进行分配内存机制,源码如下:
代码总体逻辑
JVM 再分配内存时,总是优先使用快分配策略,当快分配失败时,才会启用慢分配策略。
如果 Java 类没有被解析过,直接进入慢分配逻辑。
快速分配策略,如果没有开启栈上分配或者不符合条件则会进行 TLAB 分配。
快速分配策略,如果 TLAB 分配失败,则尝试 Eden 区分配。
如果 Eden 区分配失败,则进入慢分配策略。
如果对象满足直接进入老年代的条件,那就直接进入老年代分配。
快速分配,对于热点代码,如果开启逃逸分析,JVM 自会执行栈上分配或者标量替换等优化方案。
在某些场景使用栈上分配
设置 JVM 运行参数:
-Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:-UseTLAB -XX:+PrintGC
开启逃逸模式,关闭 TLAB
运行结果
jstat -gc pid
查看内存使用情况:
结论
看出栈上分配机制的速度非常快,只需要 6ms 就完成了实现 GC
调整 JVM 运行参数
关闭逃逸模式,开启 TLAB
-Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+UseTLAB -XX:+PrintGC
查看内存使用情况:
运行结果
结论
可以看出来,关闭了栈上分配后,不但 YGC 次数增加了,并且总体事件也变长了,总体事件 894ms
调整 JVM 运行参数
-Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:-UseTLAB -XX:+PrintGC
关闭逃逸,关闭 TLAB
运行结果
查看内存使用情况:
运行结果对比
运行耗时(开启逃逸 VS 关闭逃逸(开启 TLAB)VS 关闭逃逸(关闭 TLAB)): 6ms VS 894ms VS 1718ms
虚拟机内存 &回收(开启逃逸 VS 关闭逃逸):
调整分配空间大小
运行结果
调整启动参数: -XX:+DoEscapeAnalysis -XX:-UseTLAB
运行结果:
经过对比得出结论:
分配内存为>64byte == -XX:-UseTLAB
经过多次测试发现当_1B=64b 时效率还是非常高,一旦大于 64b 就会急剧下降。所以推断出 64byte 是 JVM 选择是 TLAB 分配 OR Eden 区分配的临界值。
TLAB 的基本介绍
TLAB(Thread Local Allocation Buffer)
线程本地分配缓存,这是一个线程独享的内存分配区域。
特点
TLAB 解决了:直接在线程共享堆上安全分配带来的线程同步性能消耗问题(解决了指针碰撞)。
TLAB 内存空间位于 Eden 区。
默认 TLAB 大小为占用 Eden Space 的 1%。
开启 TLAB 的参数
-XX:+UseTLAB
-XX:+TLABSize
-XX:TLABRefillWasteFraction
-XX:TLABWasteTargetPercent
-XX:+PrintTLAB
TLAB 的源码
TLAB 的数据结构
_start 指 TLAB 连续内存起始地址。
_top 指 TLAB 当前分配到的地址。
_end 指 TLAB 连续内存截止地址。
_desired_size 是指 TLAB 的内存大小。
_refill_waste_limit 是指最大的浪费空间。默认值为 64b
eg:假设为_refill_waste_limit=5KB:
假如当前 TLAB 已经分配 96KB,还剩下 4KB 可分配,但是现在 new 了一个对象需要 6KB 的空间,显然 TLAB 的内存不够了,4kb<5kb 这时只浪费 4KB 的空间,在_refill_waste_limit 之内,这时可以申请一个新的 TLAB 空间,原先的 TLAB 交给 Eden 管理。
假如当前 TLAB 已经分配 90KB,还剩下 10KB,现在 new 了一个对象需要 11KB,显然 TLAB 的内存不够了,这时就不能简单的抛弃当前 TLAB,这 11KB 会被安排到 Eden 区进行申请。
分配规则
obj_size + tlab_top <= tlab_end,直接在 TLAB 空间分配对象。
obj_size + tlab_top >= tlab_end && tlab_free > tlab_refill_waste_limit,
对象不在 TLAB 分配,在 Eden 区分配。(tlab_free:剩余的内存空间,tlab_refill_waste_limit:允许浪费的内存空间)
tlab 剩余可用空间>tlab 可浪费空间,当前线程不能丢弃当前 TLAB,本次申请交由 Eden 区分配空间。
obj_size + tlab_top >= tlab_end && tlab_free < _refill_waste_limit,重新分配一块 TLAB 空间,在新的 TLAB 中分配对象。
tlab 剩余可用空间<tlab 可浪费空间,在当前允许可浪费空间内,重新申请一个新 TLAB 空间,原 TLAB 交给 Eden。
清单:/src/share/vm/memory/ThreadLocalAllocationBuffer.inline.hpp
功能:TLAB 内存分配
实际上虚拟机内部会维护一个叫作 refill_waste 的值,当剩余对象空间大于 refill_waste 时,会选择在堆中分配,若小于该值,则会废弃当前 TLAB,新建 TLAB 来分配对象。
这个阈值可以使用 TLABRefillWasteFraction 来调整,它表示 TLAB 中允许产生这种浪费的比例。
默认值为 64,即表示使用约为 1/64 的 TLAB 空间作为 refill_waste。
TLAB 和 refill_waste 都会在运行时不断调整的,使系统的运行状态达到最优。
如果想要禁用自动调整 TLAB 的大小,可以使用-XX:-ResizeTLAB 禁用 ResizeTLAB
使用-XX:TLABSize 手工指定一个 TLAB 的大小。
指针碰撞 &Eden 区分配
Eden 区指针碰撞,需要模拟多线程并发申请内存空间。
且需要关闭逃逸分析 -XX:-DoEscapeAnalysis -XX:+UseTLAB
运行结果
关闭逃逸和 TLAB 分配 -XX:-DoEscapeAnalysis -XX:-UseTLAB 运行结果:
经过对比,相差 7 倍左右。二者内存回收♻️,从 YoungGC 次数和耗时上没有太大变化:应为都是 Eden 区分配。
G1 垃圾回收过程
触发混合回收条件:
-XX:InitiatingHeapOccupancyPercent=45 ,当老年代空间使用占整个堆空间 45%时。
混合回收范围:
新生代、老年代、大对象。
混合回收过程:
初始标记:
这个过程会 STW,停止系统线程。
标记 GC-Roots 的直接引用对象。
线程栈中局部变量表 。
方法区中的静态变量/常量等。
本地方法栈。
特点:速度极快。
并发标记
这个过程不会 STW,系统线程正常运行。
从第一阶段标记的 GC-Roots 开始追踪所有存活对象。
特点:慢,很耗时。
优化:JVM 会对“并发标记”阶段新产生的对象及对象修改做记录(RememberSet)
最终标记:
这个过程会 STW,系统线程停止运行。
会根据“并发标记”阶段记录的 RememberSet 进行对象标记。
特点:很快。
RememberSet 相当于是拿空间换时间。
混合回收:
这个过程会 STW,系统线程停止运行。
会计算老年代中每个 Region 中存活对象数量,存活对象占比,执行垃圾回收预期耗时和效率。
耗时:会根据启动参数中
-XX:MaxGCPauseMillis=200
和历史回收耗时来计算本次要回收多少老年代 Region 才能耗时 200ms。特点:回收了一部分远远没有达到回收的效果,G1 还有一个特殊处理方法,STW 后进行回收,然后恢复系统线程,然后再次 STW,执行混合回收掉一部分 Region,
‐XX:G1MixedGCCountTarget=8
(默认是 8 次),反复执行上述过程 8 次。
注意:假设要回收 400 个 Region,如果受限 200ms,每次只能回收 50 个 Region,反复 8 次刚好全部回收完毕。这么做的好处是避免单次停顿回收 STW 时间太长。
**还有一个参数要提一下
‐XX:G1HeapWastePercent=5 (默认是5%)
。混合回收是采用复制算法,把要回收的 Region 中存活的对象放入其他 Region 中。
然后这个 Region 中的垃圾全部清理掉,这样就会不断有 Region 释放出来,当释放出的 Region 占整个堆空间 5%时,停止混合回收。
还有一个参数:
‐XX:G1MixedGCLiveThresholdPercent=85 (默认值85%)
。回收 Region 的时候,必须是存活对象低于 85%。
混合回收失败时:
在 Mixed 回收的时候,无论是年轻代还是老年代都是基于复制算法进行回收,都要把各个 Region 的存活对象拷贝到另外其他的 Region 里去,万一拷贝是发生空间不足,就会触发一次一次失败。
一旦回收失败,立马就会切换采用 Serial 单线程进行标记+清理+整理,整个过程是非常慢的(灾难)。
版权声明: 本文为 InfoQ 作者【李浩宇/Alex】的原创文章。
原文链接:【http://xie.infoq.cn/article/1fd062a38521c492680574450】。文章转载请联系作者。
评论