• JVM性能调优-二.流程步骤实践


    紧接上一回  JVM性能调优-一.思路方向

    目录

    性能优化的步骤

    第1步:熟悉业务场景

    第2步(发现问题):性能监控

    第3步(排查问题):性能分析

    第4步(解决问题):性能调优

    GC日志分析

    GC日志参数

    测试

    GC分类

    日志结构

    OOM:堆溢出

    模拟堆溢出

    dump文件分析

    gc日志分析

    OOM:元空间溢出

    模拟元空间溢出

    dump文件分析

    原因及解决方案

    OOM:GC overhead limit exceeded

    模拟代码

    dump文件分析

    解决方案

    OOM:线程溢出

    模拟代码

    原因和解决方案

    性能优化一:合理配置堆内存

    分析

    总结

    性能优化二:JIT优化

    性能优化三:分析CPU占用超高


        JVM(Java虚拟机)调优是为了优化Java应用程序的性能和稳定性。JVM调优的目的是通过调整JVM的配置参数和优化应用程序代码,使其在给定的硬件和软件环境下达到更好的性能表现。防止出现OOM,进行JVM规划和预调优,解决程序中出现的各种OOM,减少FullGC出现的频率,解决运行慢、卡顿等问题。

    性能优化的步骤

    第1步:熟悉业务场景

    考虑以下几个方面

    • 了解核心业务功能:明确哪些部分是业务流程中的关键,哪些部分对用户体验和效率影响较大。

    • 确定性能指标:根据业务需求和用户期望,确定关键的性能指标,如响应时间、吞吐量、并发用户数等。

    • 分析用户行为和使用模式:了解用户的行为模式、访问频率和使用习惯,以便更好地规划性能优化策略。

    • 考虑未来的增长和扩展:预测业务的增长趋势和扩展需求,确保优化措施具备可扩展性和适应性。

    第2步(发现问题):性能监控

    • GC 频繁

    • cpu load过高

    • OOM

    • 内存泄漏

    • 死锁

    • 程序响应时间较长

    第3步(排查问题):性能分析

    • 打印GC日志,通过GCviewer或者 http://gceasy.io来分析日志信息

    • 灵活运用 命令行工具,jstack,jmap,jinfo等

    • dump出堆文件,使用内存分析工具分析文件,比如jconsole/ jvisualvm / jprofiler / MAT

    • 使用阿里Arthas,或jconsole,JVisualVM来实时查看JVM状态

    • jstack查看堆栈信息

    第4步(解决问题):性能调优

    • 适当增加内存,根据业务背景选择垃圾回收器

    • 优化代码,控制内存使用

    • 增加机器,分散节点压力

    • 合理设置线程池线程数量

    • 使用中间件提高程序效率,比如缓存,消息队列等

    GC日志分析

    GC日志参数

    highlighter- XML

    -verbose:gc 输出gc日志信息,默认输出到标准输出
    -XX:+PrintGC 输出GC日志。类似:-verbose:gc
    -XX:+PrintGCDetails  在发生垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域分配情况
    -XX:+PrintGCTimeStamps 输出GC发生时的时间戳
    -XX:+PrintGCDateStamps  输出GC发生时的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
    -XX:+PrintHeapAtGC   每一次GC前和GC后,都打印堆信息
    -Xloggc:  表示把GC日志写入到一个文件中去,而不是打印到标准输出中

    测试

    添加运行时参数

    -Xms60m -Xmx60m  -XX:+PrintGCDetails

    输出

    也可以导出日志文件,上传到http://gceasy.io进行在线分析

    -Xms60m -Xmx60m  -XX:+PrintGCDetails -Xloggc:D:\testGC.log

    GC分类

    • 部分收集(Partial GC):不是完整收集整个Java堆的垃圾收集。其中又分为:

    • 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有CMS GC会有单独收集老年代的行为。注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
    • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为

    • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

    日志结构

    MinorGC(或young GC或YGC)日志:

    [GC (Allocation Failure) [PSYoungGen: 31744K->2192K(36864K)] 31744K->2200K(121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs] 

    Full GC日志介绍:

    [Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K(132096K)] [ParOldGen: 416K->5453K(50176K)] 5520K->5453K(182272K), [Metaspace: 20637K->20637K(1067008K)], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 

    OOM:堆溢出

    模拟堆溢出

    1. @RequestMapping("/add")
    2. public void addObject(){
    3. System.err.println("add"+peopleSevice);
    4. ArrayList people = new ArrayList<>();
    5. while (true){
    6. people.add(new People());
    7. }
    8. }

    初始参数配置:

    1. -Xms30M -Xmx30M
    2. // 发生oom会导出一个dump文件
    3. -XX:+PrintGCDetails -XX:MetaspaceSize=64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdump.hprof
    4. -XX:+PrintGCDateStamps -Xms200M -Xmx200M -Xloggc:log/gc-oomHeap.log

    报错信息

    java.lang.OutOfMemoryError: Java heap space

    dump文件分析

    • jvisualvm工具分析堆内存文件heapdump.hprof

    • 使用MAT工具查看,能找到对应的线程及相应线程中对应实例的位置和代码:

    gc日志分析

    将日志文件导入http://gceasy.io 上面讲了

    运行时参数如果没设置堆转储,Jmap命令导出一个即可

    1. jmap -dump:format=b,file=
    2. jmap -dump:live,format=b,file=

    OOM:元空间溢出

    元空间存储数据类型

    方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

    Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

    模拟元空间溢出

    1. @RequestMapping("/metaSpaceOom")
    2. public void metaSpaceOom(){
    3. ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
    4. while (true){
    5. Enhancer enhancer = new Enhancer();
    6. enhancer.setSuperclass(People.class);
    7. enhancer.setUseCache(false);
    8. enhancer.setUseCache(true);
    9. enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
    10. System.out.println("我是加强类,输出print之前的加强方法");
    11. return methodProxy.invokeSuper(o,objects);
    12. });
    13. People people = (People)enhancer.create();
    14. people.print();
    15. System.out.println(people.getClass());
    16. System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
    17. System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
    18. System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
    19. }
    20. }

    初始参数

    1. -XX:+PrintGCDetails -XX:MetaspaceSize=60m -XX:MaxMetaspaceSize=60m -Xss512K -XX:+HeapDumpOnOutOfMemoryError
    2. -XX:HeapDumpPath=heap/heapdumpMeta.hprof -XX:SurvivorRatio=8 -XX:+TraceClassLoading -XX:+TraceClassUnloading
    3. -XX:+PrintGCDateStamps -Xms60M -Xmx60M -Xloggc:log/gc-oomMeta.log

    dump文件分析

    JDK8后,元空间替换了永久代,元空间使用的是本地内存

    原因及解决方案

    原因:

    1. 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载

    2. 应用长时间运行,没有重启

    3. 元空间内存设置过小

    解决方法

    因为该 OOM 原因比较简单,解决方法有如下几种:

    1. 检查是否永久代空间或者元空间设置的过小

    2. 检查代码中是否存在大量的反射操作

    3. dump之后通过mat检查是否存在大量由于反射生成的代理类

    OOM:GC overhead limit exceeded

    模拟代码

    1. public class OOMTest {
    2. public static void main(String[] args) {
    3. test1();
    4. // test2();
    5. }
    6. public static void test1() {
    7. int i = 0;
    8. List list = new ArrayList<>();
    9. try {
    10. while (true) {
    11. list.add(UUID.randomUUID().toString().intern());
    12. i++;
    13. }
    14. } catch (Throwable e) {
    15. System.out.println("************i: " + i);
    16. e.printStackTrace();
    17. throw e;
    18. }
    19. }
    20. public static void test2() {
    21. String str = "";
    22. Integer i = 1;
    23. try {
    24. while (true) {
    25. i++;
    26. str += UUID.randomUUID();
    27. }
    28. } catch (Throwable e) {
    29. System.out.println("************i: " + i);
    30. e.printStackTrace();
    31. throw e;
    32. }
    33. }
    34. }

    代码解析

    第一段代码:运行期间将内容放入常量池的典型案例

    intern()方法

    • 如果字符串常量池里面已经包含了等于字符串X的字符串,那么就返回常量池中这个字符串的引用;

    • 如果常量池中不存在,那么就会把当前字符串添加到常量池并返回这个字符串的引用

    第二段代码:不停的追加字符串str

    为什么第二个没有报GC overhead limit exceeded呢?以上两个demo的区别在于:

    • Java heap space的demo每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出

    • GC overhead limit exceeded的demo由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。

    报错信息:

    [Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7110K->7095K(7168K)] 9158K->9143K(9728K), [Metaspace: 3177K->3177K(1056768K)], 0.0479640 secs] [Times: user=0.23 sys=0.01, real=0.05 secs] 
    java.lang.OutOfMemoryError: GC overhead limit exceeded
    [Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7114K->7096K(7168K)] 9162K->9144K(9728K), [Metaspace: 3198K->3198K(1056768K)], 0.0408506 secs] [Times: user=0.22 sys=0.01, real=0.04 secs] 
    
    通过查看GC日志可以发现,系统在频繁性的做FULL GC,但是却没有回收掉多少空间,那么引起的原因可能是因为内存不足,也可能是存在内存泄漏的情况,接下来我们要根据堆DUMP文件来具体分析

    dump文件分析

    和上面两个相同,可自行分析

    解决方案

    原因:

    这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出

    解决方法:

    1. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。

    2. 添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。

    3. dump内存,检查是否存在内存泄漏,如果没有,加大内存。

    OOM:线程溢出

    模拟代码

    1. public class TestNativeOutOfMemoryError {
    2. public static void main(String[] args) {
    3. for (int i = 0; ; i++) {
    4. System.out.println("i = " + i);
    5. new Thread(new HoldThread()).start();
    6. }
    7. }
    8. }
    9. class HoldThread extends Thread {
    10. CountDownLatch cdl = new CountDownLatch(1);
    11. @Override
    12. public void run() {
    13. try {
    14. cdl.await();
    15. } catch (InterruptedException e) {
    16. }
    17. }
    18. }

    报错信息

    java.lang.OutOfMemoryError : unable to create new native Thread

    原因和解决方案

    出现这种异常,基本上都是创建了大量的线程导致的

    解决一

    通过 -Xss 设置每个线程栈大小的容量

    • JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。

    • 正常情况下,在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

    • 能创建的线程数的具体计算公式如下:

    (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) =线程数量

    ——————————————————————————————————————

    MaxProcessMemory: 进程/应用程序可用的最大内存量。

    JVMMemory: 分配给Java虚拟机(JVM)的内存。

    ReservedOsMemory: 操作系统保留的内存,供其自身使用。

    ThreadStackSize: 每个线程的栈内存大小。

    ——————————————————————————————————————

        通过从最大进程内存中减去JVM内存和保留的操作系统内存,可以得到创建线程所需的可用内存。将该可用内存除以线程栈的大小,可以估算出可创建的线程数量。

        然而,需要注意的是,这个公式只是一个粗略的估算,并不总是准确反映实际可创建的线程数量。在确定应用程序的最佳线程数时,还需要考虑其他因素,如系统资源、CPU容量和其他特定于应用程序的限制。此外,为了避免资源耗尽或性能下降,还应根据性能测试和分析来监控和调整线程数。

    解决二

    • 在Java语言里, 当你创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统线程的内存用的不是JVMMemory,而是系统中剩下的内存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。

    • 由公式得出结论:你给JVM内存越多,那么你能创建的线程越少,越容易发生java.lang.OutOfMemoryError: unable to create new native thread

    综上,在生产环境下如果需要更多的线程数量,建议使用64位操作系统,如果必须使用32位操作系统,可以通过调整Xss的大小来控制线程数量。

    线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:

    • /proc/sys/kernel/pid_max 系统最大pid值,在大型系统里可适当调大

    • /proc/sys/kernel/threads-max 系统允许的最大线程数

    • maxuserprocess(ulimit -u) 系统限制某用户下最多可以运行多少进程或线程

    • /proc/sys/vm/max_map_count

    max_map_count文件包含限制一个进程可以拥有的VMA(虚拟内存区域)的数量。虚拟内存区域是一个连续的虚拟地址空间区域。在进程的生命周期中,每当程序尝试在内存中映射文件,链接到共享内存段,或者分配堆空间的时候,这些区域将被创建。调优这个值将限制进程可拥有VMA的数量。限制一个进程拥有VMA的总数可能导致应用程序出错,因为当进程达到了VMA上线但又只能释放少量的内存给其他的内核进程使用时,操作系统会抛出内存不足的错误。如果你的操作系统在NORMAL区域仅占用少量的内存,那么调低这个值可以帮助释放内存给内核用。

    性能优化一:合理配置堆内存

    增加内存可以提高系统的性能而且效果显著,那么随之带来的一个问题就是,我们增加多少内存比较合适?如果内存过大,那么如果产生FullGC的时候,GC时间会相对比较长,如果内存较小,那么就会频繁的触发GC,在这种情况下,我们该如何合理的适配堆内存大小呢?

    分析

    依据的原则是根据Java Performance里面的推荐公式来进行设置。

    Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。 方法区(永久代 PermSize和MaxPermSize 或 元空间 MetaspaceSize 和 MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。 年轻代Xmn的设置为老年代存活对象的1-1.5倍。 老年代的内存大小设置为老年代存活对象的2-3倍。

    但是,上面的说法也不是绝对的,也就是说这给的是一个参考值,根据多种调优之后得出的一个结论,大家可以根据这个值来设置一下我们的初始化内存,在保证程序正常运行的情况下,我们还要去查看GC的回收率,GC停顿耗时,内存里的实际数据来判断,Full GC是基本上不能有的,如果有就要做内存Dump分析,然后再去做一个合理的内存分配。

    我们还要注意到一点就是,上面说的老年代存活对象怎么去判定

    判定:

    JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)。

    总结

    在内存相对紧张的情况下,可以按照上述的方式来进行内存的调优, 找到一个在GC频率和GC耗时上都可接受的一个内存设置,可以用较小的内存满足当前的服务需要。

    但当内存相对宽裕的时候,可以相对给服务多增加一点内存,可以减少GC的频率,GC的耗时相应会增加一些。 一般要求低延时的可以考虑多设置一点内存, 对延时要求不高的,可以按照上述方式设置较小内存。

    如果在垃圾回收日志中观察到OutOfMemoryError,尝试把Java堆的大小扩大到物理内存的80%~90%。尤其需要注意的是堆空间导致的OutOfMemoryError以及一定要增加空间。

    • 比如说,增加-Xms和-Xmx的值来解决old代的OutOfMemoryError

    • 增加-XX:PermSize和-XX:MaxPermSize来解决permanent代引起的OutOfMemoryError(jdk7之前);增加-XX:MetaspaceSize和-XX:MaxMetaspaceSize来解决Metaspace引起的OutOfMemoryError(jdk8之后)

    记住一点Java堆能够使用的容量受限于硬件以及是否使用64位的JVM。在扩大了Java堆的大小之后,再检查垃圾回收日志,直到没有OutOfMemoryError为止。如果应用运行在稳定状态下没有OutOfMemoryError就可以进入下一步了,计算活动对象的大小。

    性能优化二:JIT优化

    可见这篇文章的逃逸分析内容

    性能优化三:分析CPU占用超高

    如果是生产环境的话,是怎么样才能发现目前程序有问题呢?我们可以推导一下,如果线程死锁,那么线程一直在占用CPU,这样就会导致CPU一直处于一个比较高的占用率。所示我们解决问题的思路应该是:

    1.查看所有java进程 ID

    jps -l

    2.根据进程 ID 检查当前使用异常线程的pid

    top -Hp 1456

    3.当前占用cpu比较高的线程 ID 是1465

    接下来把 线程 PID 转换为16进制为

    # 10 进制线程PId 转换为 16 进制
    1465   ------->    5b9
    # 5b9 在计算机中显示为   
    0x5b9

    4.最后我们把线程信息打印出来:

    jstack 1456 > jstack.log

    5.打开jstack.log文件 查找一下刚刚我们转换完的16进制ID是否存在

    jstack命令生成的thread dump信息包含了JVM中所有存活的线程,里面确实是存在我们定位到的线程 ID ,在thread dump中每个线程都有一个nid,在nid=0x5b9的线程调用栈中,我们发现两个线程在互相等待对方释放资源

    xxx大厂问题排查过程:
    ...省略;
    4、ps aux | grep java  查看到当前java进程使用cpu、内存、磁盘的情况获取使用量异常的进程
    5、top -Hp 进程pid  检查当前使用异常线程的pid
    6、把线程pid变为16进制如 31695 - 》 7bcf  然后得到0x7bcf
    7、jstack 进程的pid | grep -A20  0x7bcf  得到相关进程的代码
  • 相关阅读:
    制作自定义版本 kernel 镜像
    7 判断给定的二叉树是否是二叉排序树---来源刘H同学
    变量、流程控制与游标(MySQL)
    Web前端-Vue2+Vue3基础入门到实战项目-Day5(自定义指令, 插槽, 案例商品列表, 路由入门)
    LabVIEW Modbus通讯稳定性提升
    Shiro学习(10)Session管理
    springcloud4:服务注册中心Eureka
    金九银十进大厂必刷的Java面试题 (全彩版)
    Flink日志收集到数据库/kafka
    dockerfile 中激活conda并安装package
  • 原文地址:https://blog.csdn.net/qq_42428269/article/details/132828374