1 垃圾回收器组合(内存碎片)
垃圾回收器从线程运行情况分类有三种
串行回收, Serial 回收器, 单线程回收, 全程 stw;
并行回收, 名称以 Parallel 开头的回收器, 多线程回收, 全程 stw;
并发回收, cms 与 G1, 多线程分阶段回收, 只有某阶段会 stw;
cms 只会回收老年代和永久带(1.8 开始为元数据区, 需要设置 CMSClassUnloadingEnabled), 不会收集年轻带;
cms 是一种预处理垃圾回收器, 它不能等到 old 内存用尽时回收, 需要在内存用尽前, 完成回收操作, 否则会导致并发回收失败; 所以 cms 垃圾回收器开始执行回收操作, 有一个触发阈值, 默认是老年代或永久带达到 92%;
垃圾回收的四个主要阶段:
初始标记 初识标记: 这个过程是标记从 gc root 出发发的直接相关的引用. 这个时间很短, 但是是 stop the world;
并发标记 并发标记: 用户线程并行执行, 进行相关的引用标记. 这个时间很长, 一般决定于堆内存的大小. 所使用的线程数为(CPU 个数 + 3)/4, 所以当 CPU 核数很少时, 在并发标记阶段会出现严重的性能下降. 为了解决这个问题, 对于 CPU 核数很少时, 在并发标记阶段会与用户线程交叉执行, 以使服务器性能不至于下降的太严重. 但是这样操作会使标记过程所耗费的时间更长.
重新标记 重新标记: 因为在并发标记时, 用户线程在执行, 可能会造成再次的实例引用. 所以需要重新标记一下. 这个阶段的标记也是 stop the world, 并且是并行标记.
并发清除 并发清除, 即清除相关的垃圾.
CMS 的缺点:
由于 CMS 使用的是标记清除算法, 会造成内存碎片, 当老年代无法再次分配内存时需要 FULL GC.CMS 提供了一个参数 - XX+UseCMSCompactAtFullCollection, 即在执行 FULL GC 时开启内存碎片的合并整理过程. 这也会引起 stop the world.
CMS 在进行垃圾回收时, 无法处理浮动垃圾. 所以在进行垃圾回收时, 需要留有一定的内存供用户线程使用. CMS 提供了一个内存触发垃圾回收的内存使用比例: -XX:CMSInitiatingOccupancyFraction, 如果预留的内存不够使用, 就会出现 Cocurrent Mode Failure 失败, 这时就需要启动后备预案: 临时使用串行收集器重新进行老年代的垃圾收回, 这个时间更长.
CMS 用来进行老年代的垃圾回收, 这个与 ParNew(多线程的串行垃圾回收)进行组合, 用于整个的堆内存的垃圾回收.
2 FULL GC 在大内存处理的无力感
随着硬件的进步, 32GB,64GB 甚至 100GB 的内存已经很多了, 基于 JVM 的 CMS 垃圾回收已经有种无力感, FULL GC 的时间遵循 8-10 秒 / G 的压迫. 100GB 简直就是灾难啊, 800-1000 秒, 可是都接近甚至大于 10 分钟了啊.
同步模式失败: CMS 在没有完全释放老年代的内存空间时, 新生代对象要过快转化, 导致此时的收集器停止并发收集过程, 转化为单线程的暂停, 可谓是一触即发 Ful GC.
碎片化造成新生代升到老年代的对象比老年代所有可以使用的连续空间都大. 比如: 老年代有 500MB 的空间可以使用, 但是都是 1KB 的碎片空间, 现在有一个 2KB 的新生代对象转换为老年代对象, 此时因为没有 2KB 的连续空间, 所以不得不 FullGC.
MemStore 会定期刷写成为一个 HFile, 在刷写的同时这个 Memstore 所占用的内存空间就会被标记为待回收, 但是因为是按照顺序的, 所以会出现以下情况.
此时老年代若都是 1KB 的碎片空间, 现在有一个 2KB 的新生代对象转换为老年代对象, 此时因为没有 2KB 的连续空间, 所以不得不 FullGC.
因此朱丽叶暂停就诞生了, 危机越来越严重.
基于此, JVM 想到了线程解决方案, 叫做 TLAB(Thread-Local allocation Buffer), 当使用 TLAB 时, 每一个线程都会分配一个固定大小的内存空间, 但是缺点就是无论你的线程里面有没有对象, 其中有很大一部分空间都是闲置的, 内存空间的利用率就会降低.
3 HBase 的 TLAB 的升华 MSLAB
因为 HBase 中多个 Region 是被一个线程管理的, 但是多个 MemStore 占用的内存还是无法合理的分开, 于是 Hbase 就自己实现了一套以 Memstore 为最小单元格的内存管理机制, 叫做 MSLSB(MemStore-Local Allocation Buffers). 这套思路即来自 TLAB, 只不过内存空间是由 MemStore 来分配.
MLSB 引入 chunk 的概念, 所谓 chunk 就是一块内存, 大小默认为 2MB.
RegionServer 中维护者一个全局的 MemStoreChunkPool 实例, 从名字上很容易看出, 是一个 Chunk 池.
每一个 MemStore 里面都会有一个 MemStoreLAB 实例.
当 MemStore 接收到 KeyValue 数据的时候先从 ChunkPool 中申请了一个 chunk, 然后放到 MemStoreLAB 实例中.
一旦 MemStoreLAB 实例中放满了, 就新申请一个新的.
如果 MemStore 因为刷写而释放内存, 则按 chunk 来清空内存.
上面的流程就解决了小碎片引起的无法插入大数据的问题,
4 MSLAB 的参数设置
hbase.hregion.memstore.mslab.enabled: 设置为 true, 即打开 MSLAB, 默认是 true.
hbase.hregion.memstore.chunkpool.maxsize: 表示在整个 Memstore 可以占用的堆内存的比例. 默认值是 0, 因此设置大于 0, 才算真正开启 MSLAB.
hregion.memstore.chunkpool.initialsize: 表示在 RegionServer 启动的时候预分配一些 chunk 出来. 也是一个比例值, 该值表示预分配的 chunk 占用总的 chunkpool 的大小.
hbase.hregion.memstore.mslab.chunksize: 每一个 chunk 的大小, 默认是 2048*1024, 即 2MB.
hbase.hregion.memstore.mslab.max.allocation: 能放入 chunk 的最大单元格大小, 默认是 256KB, 已经很大了.
来源: https://juejin.im/post/5c024eedf265da611e4d64f4