【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(二)

简介: 【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍

【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:

  1. 假如当前TLAB已经分配96KB,还剩下4KB可分配,但是现在new了一个对象需要6KB的空间,显然TLAB的内存不够了,4kb<5kb这时只浪费4KB的空间,在_refill_waste_limit之内,这时可以申请一个新的TLAB空间,原先的TLAB交给Eden管理。
  2. 假如当前TLAB已经分配90KB,还剩下10KB,现在new了一个对象需要11KB,显然TLAB的内存不够了,这时就不能简单的抛弃当前TLAB,这11KB会被安排到Eden区进行申请。

分配规则

  1. obj_size + tlab_top <= tlab_end,直接在TLAB空间分配对象。
  2. obj_size + tlab_top >= tlab_end && tlab_free > tlab_refill_waste_limit,
  • 对象不在TLAB分配,在Eden区分配。(tlab_free:剩余的内存空间,tlab_refill_waste_limit:允许浪费的内存空间
  • tlab剩余可用空间>tlab可浪费空间当前线程不能丢弃当前TLAB本次申请交由Eden区分配空间
  1. 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%时

混合回收范围:

新生代、老年代、大对象。

混合回收过程:

初始标记:

  1. 这个过程会STW,停止系统线程。
  2. 标记GC-Roots的直接引用对象。
  1. 线程栈中局部变量表 。
  2. 方法区中的静态变量/常量等。
  3. 本地方法栈。
  • 特点:速度极快。

并发标记

  1. 这个过程不会STW,系统线程正常运行
  2. 从第一阶段标记的GC-Roots开始追踪所有存活对象
  • 特点:慢,很耗时
  • 优化:JVM会对“并发标记”阶段新产生的对象及对象修改做记录(RememberSet)

最终标记:

  1. 这个过程会STW,系统线程停止运行。
  2. 会根据“并发标记”阶段记录的RememberSet进行对象标记。
  • 特点:很快。
  • RememberSet相当于是拿空间换时间

混合回收:

  1. 这个过程会STW,系统线程停止运行
  2. 会计算老年代中每个Region中存活对象数量,存活对象占比,执行垃圾回收预期耗时和效率
  • 耗时:会根据启动参数中-XX:MaxGCPauseMillis=200和历史回收耗时来计算本次要回收多少老年代Region才能耗时200ms
  • 特点:回收了一部分远远没有达到回收的效果,G1还有一个特殊处理方法,STW后进行回收,然后恢复系统线程,然后再次STW,执行混合回收掉一部分Region,‐XX:G1MixedGCCountTarget=8 (默认是8次),反复执行上述过程8次

注意:假设要回收400个Region,如果受限200ms,每次只能回收50个Region,反复8次刚好全部回收完毕。这么做的好处是避免单次停顿回收STW时间太长

  1. **还有一个参数要提一下‐XX:G1HeapWastePercent=5 (默认是5%)
  • 混合回收是采用复制算法,把要回收的Region中存活的对象放入其他Region中
  • 然后这个Region中的垃圾全部清理掉,这样就会不断有Region释放出来,当释放出的Region占整个堆空间5%时,停止混合回收
  1. 还有一个参数:‐XX:G1MixedGCLiveThresholdPercent=85 (默认值85%) 。回收Region的时候,必须是存活对象低于85%。

混合回收失败时:

  1. 在Mixed回收的时候,无论是年轻代还是老年代都是基于复制算法进行回收,都要把各个Region的存活对象拷贝到另外其他的Region里去,万一拷贝是发生空间不足,就会触发一次失败
  2. 一旦回收失败,立马就会切换采用Serial 单线程进行标记+清理+整理,整个过程是非常慢的(灾难)。
相关文章
|
19天前
|
存储 算法 Java
JVM: 内存、类与垃圾
分代收集算法将内存分为新生代和老年代,分别使用不同的垃圾回收算法。新生代对象使用复制算法,老年代对象使用标记-清除或标记-整理算法。
22 6
|
19天前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
102 0
|
3月前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
3月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
4月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
4月前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
64 1
|
4月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
43 3
|
4月前
|
Java
实现java执行kettle并传参数
实现java执行kettle并传参数
51 1
|
4月前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
13天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
129 60
【Java并发】【线程池】带你从0-1入门线程池

热门文章

最新文章