【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)https://developer.aliyun.com/article/1471126
运行结果
-XX:+DoEscapeAnalysis -XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC -XX:-UseTLAB
[GC (Allocation Failure) 1023K->516K(5632K), 0.0028410 secs] [GC (Allocation Failure) 1540K->578K(5632K), 0.0023265 secs] ........ [GC (Allocation Failure) 2466K->1442K(5632K), 0.0013395 secs] [GC (Allocation Failure) 2466K->1442K(5632K), 0.0004367 secs] 8925
调整启动参数: -XX:+DoEscapeAnalysis -XX:-UseTLAB
运行结果:
ruby
复制代码
-XX:+DoEscapeAnalysis -XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC -XX:-UseTLAB
csharp
复制代码
[GC (Allocation Failure) 1023K->516K(5632K), 0.0028410 secs] [GC (Allocation Failure) 1540K->578K(5632K), 0.0023265 secs] ........ [GC (Allocation Failure) 2466K->1442K(5632K), 0.0013395 secs] [GC (Allocation Failure) 2466K->1442K(5632K), 0.0004367 secs] 8925
经过对比得出结论:
分配内存为>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的数据结构
c
复制代码
class ThreadLocalAllocBuffer: public CHeapObj<mtThread> { HeapWord* _start; // address of TLAB HeapWord* _top; // address after last allocation HeapWord* _pf_top; // allocation prefetch watermark HeapWord* _end; // allocation end (excluding alignment_reserve) size_t _desired_size; // desired size (including alignment_reserve) size_t _refill_waste_limit; // hold onto tlab if free() is larger than this }
- _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内存分配
c
复制代码
inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) { invariants(); // 获取当前top HeapWord* obj = top(); if (pointer_delta(end(), obj) >= size) { // successful thread-local allocation #ifdef ASSERT // Skip mangling the space corresponding to the object header to // ensure that the returned space is not considered parsable by // any concurrent GC thread. size_t hdr_size = oopDesc::header_size(); Copy::fill_to_words(obj + hdr_size, size - hdr_size, badHeapWordVal); #endif // ASSERT // This addition is safe because we know that top is // at least size below end, so the add can't wrap. // 重置top set_top(obj + size); invariants(); return obj; } return NULL; }
实际上虚拟机内部会维护一个叫作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区分配
java
复制代码
// 指针碰撞分配 HeapWord* compare_to = *Universe::heap()->top_addr(); HeapWord* new_top = compare_to + obj_size; if (new_top <= *Universe::heap()->end_addr()) { if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) { goto retry; } result = (oop) compare_to; } }
Eden区指针碰撞,需要模拟多线程并发申请内存空间。
java
复制代码
/** * @since 2019/8/19 下午11:25 -Xmx100m -Xms100m -XX:-DoEscapeAnalysis -XX:+UseTLAB -XX:TLABWasteTargetPercent=1 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails */ public class AllocationTLABSomeThread { private static final int threadNum = 100; private static CountDownLatch latch = new CountDownLatch(threadNum); private static final int n = 50000000 / threadNum; private static void alloc() { byte[] b = new byte[100]; } public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < threadNum; i++) { new Thread(() -> { for (int j = 0; j < n; j++) { alloc(); } latch.countDown(); }).start(); } try { latch.await(); } catch (InterruptedException e) { System.out.println("hello world"); } long end = System.currentTimeMillis(); System.out.println(end - start); } }
且需要关闭逃逸分析 -XX:-DoEscapeAnalysis -XX:+UseTLAB
运行结果
ruby
复制代码
-XX:-DoEscapeAnalysis -XX:InitialHeapSize=104857600 -XX:MaxHeapSize=104857600 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:TLABWasteTargetPercent=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC -XX:+UseTLAB [GC (Allocation Failure) [PSYoungGen: 25600K->960K(29696K)] 25600K->968K(98304K), 0.0019559 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 26560K->960K(29696K)] 26568K->968K(98304K), 0.0022243 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 26560K->768K(29696K)] 26568K->776K(98304K), 0.0022446 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] ........ [GC (Allocation Failure) [PSYoungGen: 32768K->0K(33280K)] 34193K->1425K(101888K), 0.0014598 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 32768K->0K(33280K)] 34193K->1425K(101888K), 0.0015168 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] 823 Heap PSYoungGen total 33280K, used 3655K [0x00000007bdf00000, 0x00000007c0000000, 0x00000007c0000000) eden space 32768K, 11% used [0x00000007bdf00000,0x00000007be291c48,0x00000007bff00000) from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000) to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000) ParOldGen total 68608K, used 1425K [0x00000007b9c00000, 0x00000007bdf00000, 0x00000007bdf00000) object space 68608K, 2% used [0x00000007b9c00000,0x00000007b9d64798,0x00000007bdf00000) Metaspace used 4255K, capacity 4718K, committed 4992K, reserved 1056768K class space used 477K, capacity 533K, committed 640K, reserved 1048576K
关闭逃逸和TLAB分配 -XX:-DoEscapeAnalysis -XX:-UseTLAB 运行结果:
ruby
复制代码
-XX:-DoEscapeAnalysis -XX:InitialHeapSize=104857600 -XX:MaxHeapSize=104857600 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:TLABWasteTargetPercent=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC -XX:-UseTLAB [GC (Allocation Failure) [PSYoungGen: 25599K->976K(29696K)] 25599K->984K(98304K), 0.0023516 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 26575K->880K(29696K)] 26583K->888K(98304K), 0.0015459 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 26480K->832K(29696K)] 26488K->840K(98304K), 0.0006776 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] ....... [GC (Allocation Failure) [PSYoungGen: 32767K->0K(33280K)] 34053K->1285K(101888K), 0.0004838 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 32767K->0K(33280K)] 34053K->1285K(101888K), 0.0005389 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 5388 Heap PSYoungGen total 33280K, used 21392K [0x00000007bdf00000, 0x00000007c0000000, 0x00000007c0000000) eden space 32768K, 65% used [0x00000007bdf00000,0x00000007bf3e4230,0x00000007bff00000) from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000) to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000) ParOldGen total 68608K, used 1285K [0x00000007b9c00000, 0x00000007bdf00000, 0x00000007bdf00000) object space 68608K, 1% used [0x00000007b9c00000,0x00000007b9d41788,0x00000007bdf00000) Metaspace used 4248K, capacity 4718K, committed 4992K, reserved 1056768K class space used 478K, capacity 533K, committed 640K, reserved 1048576K
经过对比,相差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 单线程进行标记+清理+整理,整个过程是非常慢的(灾难)。