JVM内存分配-逃逸分析、碰撞指针和TLAB详解
转自:https://www.zhifou.net/blogdetail/188
1.逃逸分析
? 在方法体中创建对象,如果该对象被方法体其他变量引用到,叫方法逃逸,被外部线程访问到叫线程逃逸。JVM如果开启了逃逸分析,JIT会对代码进行如下优化:
- 同步省略(锁消除),没有逃逸,则直接消除锁。
- 将堆分配转化为栈分配,栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后栈自动销毁,而不需要垃圾回收的介入,从而提高系统性能。
- 分离对象或标量替换,原始数据类型(int,long等数值类型以及reference类型等)不能再进一步分解,称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化, 可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。逃逸分析开关参数:
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis
锁消除开关参数:
-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除
? 锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析
2.指针碰撞
堆内存被一个指针一分为二,指针的左边都被塞满了对象,指针的右变是未使用的区域。每一次有新的对象创建,指针就会向右移动一个对象size的距离。这就被称为指针碰撞。如下图所示:
? 这里会存在两个问题,第一个问题:已用区域会随着垃圾收集,出现大量的碎片。第二个问题:如何解决并发。
? 第一个问题:已用区域会随着垃圾收集,出现大量的碎片,造成内存不连续。解决的思路是建立一个FreeList,把释放的区域加入到FreeList中,下次分配对象的时候,优先从 FreeList 中寻找合适的内存大小进行分配,之后再在主内存中撞针分配。
? 第二个问题:如何解决并发。通过CAS更新,失败了就重试。
3.TLAB
? 上面我们分析了如果没有发生逃逸,对象直接在栈上分配,没有并发问题。否则在堆中分配,多线程下有并发问题,方式时通过加锁、指针碰撞。加锁肯定性能不高。那么指针碰撞,在多线程下频繁的创建对象,CAS频繁的重试更新,性能也不高。
? 现在一般采用TLAB。通过TLAB(Thread Local Allocation Buffer),是在Hotspot1.6引入的新技术。在线程启动时,在堆的Eden区域申请一块指定大小的内存,线程私有,如果线程内需要创建对象,则在此区域内创建,避免并发,提升性能。包含start、top(归属线程最后一次申请的尾位置)、end。如下图所示:
? TLAB由于是线程私有,所以就避免了并发,性能很高。
3.1 TLAB分配方式
? 首先都会检查是否启用了 TLAB,如果启用了,则会尝试 TLAB 分配;如果当前线程的 TLAB 大小足够,那么从线程当前的 TLAB 中分配;如果不够,但是当前 TLAB 剩余空间小于最大浪费空间限制(这是一个动态的值,我们后面会详细分析),则从堆上(一般是 Eden 区) 重新申请一个新的 TLAB 进行分配。否则,直接在 TLAB 外进行分配。TLAB 外的分配策略,不同的 GC 算法不同。例如G1:
- 如果是 Humongous 对象(对象在超过 Region 一半大小的时候),直接在 Humongous 区域分配(老年代的连续区域)。
- 根据 Mutator 状况在当前分配下标的 Region 内分配。
3.2 TLAB生命周期
? TLAB 是线程私有的,线程初始化的时候,会创建并初始化 TLAB。同时,在 GC 扫描对象发生之后,线程第一次尝试分配对象的时候,也会创建并初始化 TLAB。
? TLAB 生命周期停止(TLAB 声明周期停止不代表内存被回收,只是代表这个 TLAB 不再被这个线程私有管理)在:
? 当前 TLAB 不够分配,并且剩余空间小于最大浪费空间限制,那么这个 TLAB 会被退回 Eden,重新申请一个新的 发生 GC 的时候,TLAB 被回收。
3.2 TLAB缺点
1.会造成空间浪费,并造成内存碎片。
- 当前 TLAB 不够分配时,如果剩余空间小于最大浪费空间限制,那么这个 TLAB 会被退回 Eden,重新申请一个新的。这个剩余空间就会成为碎片。
- 当发生 GC 的时候,TLAB 没有用完,没有分配的内存也会成为碎片。2.GC内存分配不稳定,因为不同业务TLAB都是变化的。
- 不同的线程业务场景不同导致分配对象大小不同。我们一般会按照业务区分不同的线程池,做好线程池隔离。对于用户请求,每次分配的对象可能比较小。对于后台分析请求,每次分配的对象相对大一些。
- 不同时间段内线程压力并不均匀。业务是有高峰有低谷的,高峰时间段内肯定分配对象更多。
- 同一时间段同一线程池内的线程的业务压力也不一定不能做到很均匀。很可能只有几个线程很忙,其他线程很闲。
所以,一般建议不能一下子就给一个线程申请一个比较大的 TLAB,而是考虑这个线程 TLAB 分配满之后再申请新的,这样更加灵活。