Java 代码监控 JVM 运行状态 —— 记一次 JVM 调优的毛招

在做模型项目的时候遇到一个问题,由于模型服务装载一些大模型,大模型对象的大小在 300M 左右,而一台服务器可能装载多个大模型。在服务启动和模型更新的时候会遇到 young gc 耗时过长的问题,young gc 所采用的垃圾回收器是 ParNew。通过观察 GC 日志可以发现,模型对象一开始是存在于年轻代的,当经过 15次 gc 后,这些对象就会进入到老年代,而之后 young gc 的时间缩短到正常可以接受的时间范围 0.01s ~ 0.02s。而在模型对象尚未进入老年代时,young gc 耗时就会超过 0.3 s,导致线上请求模型会超时。这个原因主要是年轻代回收的过程中,标记过程其实耗时并不长,而长的是 survival 区复制对象造成的耗时。

既然定位到问题,那么就需要解决问题,解决方案就是让这种大模型立马进入老年代(由于在模型加载的时候通过控制调用方的路由表保证了服务器不对外提供服务,因此在这个阶段可以让模型对象进入老年代后再提供服务)。一种方法是调整最大晋升代的阈值,如果调整过小,担心 old 区增长过快,而导致线上服务 old gc 不可控(因为每天会在非高峰期主动触发 Old GC, System.gc()),于是采用第二种方式,生产对象主动触发 Young GC,把模型对象挤入老年代。当然还有一种方法,不停的 System.gc(),这个过程相对耗时较长,而且线上服务一般会对 full gc 次数做监控,为了避免报警,所以最终没有选择这种简单粗暴的方式。

实现第二个方案需要得知 Eden 区 大小,以及最大晋升代数,例如 Eden 区有 2G,最大代数有 10代,那么就可以通过生产 20G 的对象,把模型挤入老年代,因此需要获得这两个参数。通过观察 gc 日志,以及命令都可以得知这些参数,而在运行期程序内部获得这些参数一直没有实现过。如果这些参数作为 properties,是可以通过 System.getProperties 这种方式来获取参数的。但是,如果不作为系统属性的话,该如何获取呢。这就要通过 ManagementFactory 了。

先上代码:

public class GCUtil {

    /**
     * 调用 System.gc
     */
    public static void systemGC() {
        long startTime = System.currentTimeMillis();
        LOGGER.info("Call for system gc start...");
        System.gc();
        LOGGER.info("Call for system gc end, spend {}ms", System.currentTimeMillis() - startTime);
    }

    /**
     * 保证当前年轻代进入老年代的 gc,用于模型更新以及刚上线期间
     */
    public static void youngPromoteGC() {
        try {
            long startTime = System.currentTimeMillis();
            LOGGER.info("Call for young promote gc start...");

            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            List<String> args = runtimeMXBean.getInputArguments();

            // 获取 eden 区大小
            long edenMSize = 0;
            for (String arg : args) {
                LOGGER.info("--------------- Jvm param: {}", arg);
                if (arg.startsWith(JVM_XMN)) {
                    String edenSizeStr = arg.substring(JVM_XMN.length());
                    if (edenSizeStr.toLowerCase().endsWith("g")) {
                        long edenGSize = Long.valueOf(edenSizeStr.substring(0, edenSizeStr.length() - 1));
                        edenMSize = edenGSize * 1024;
                    } else if (edenSizeStr.toLowerCase().endsWith("m")) {
                        edenMSize = Long.valueOf(edenSizeStr.substring(0, edenSizeStr.length() - 1));
                    } else {
                        LOGGER.warn("Cannot recognize -Xmn argument: " + JVM_XMN);
                    }
                }
            }

            if (edenMSize == 0) {
                edenMSize = getEdenMemorySize();
            }

            if (edenMSize <= 0) {
                LOGGER.warn("Extract jvm -Xmn failed");
                return;
            }

            // 获取 晋升老年代最大次数
            int tenuringThreshold = DEFAULT_TENURING_THRESHOLD;
            for (String arg : args) {
                if (arg.startsWith(MAX_TENURING_THRESHOLD)) {
                    String tenuringThresholdStr = arg.substring(MAX_TENURING_THRESHOLD.length() + 1);
                    tenuringThreshold = Integer.valueOf(tenuringThresholdStr);
                    if (tenuringThreshold > DEFAULT_TENURING_THRESHOLD) {
                        tenuringThreshold = DEFAULT_TENURING_THRESHOLD;
                    }
                }
            }

            LOGGER.info("Start to young gc, -Xmn={}m, -XX:MaxTenuringThreshold={}", edenMSize, tenuringThreshold);

            // 手动触发 Young GC
            for (int i = 0; i < edenMSize * tenuringThreshold; ++i) {
                allocate_1M();
            }

            // System GC,清理老年代
            System.gc();

            LOGGER.info("Call for young promote gc end, spend {}ms", System.currentTimeMillis() - startTime);
        } catch (Exception e) {
            LOGGER.error("Trigger young promote gc failed: ", e);
        }
    }

    /**
     * 获取新生代大小,单位 M
     */
    private static long getEdenMemorySize() {
        List<MemoryPoolMXBean> poolMXBeanList = ManagementFactory.getMemoryPoolMXBeans();
        for (MemoryPoolMXBean memoryPoolMXBean : poolMXBeanList) {
            if (memoryPoolMXBean.getName().toLowerCase().contains("eden")) {
                long maxUsage = memoryPoolMXBean.getUsage().getMax();
                return maxUsage >> 20;
            }
        }
        return -1;
    }

    /**
     * 生成个 1M 对象
     */
    private static void allocate_1M() {
        byte[] _1M = new byte[1024 * 1024];
    }

    private GCUtil() {
    }

    private static final String MAX_TENURING_THRESHOLD = "-XX:MaxTenuringThreshold";

    private static final int DEFAULT_TENURING_THRESHOLD = 15;

    private static final String JVM_XMN = "-Xmn";

    private static final Logger LOGGER = LoggerFactory.getLogger(GCUtil.class);

}

整体思路就是 ManagementFactory.getRuntimeMXBean().getInputArguments() 获得 List<String> 所有 JVM 参数。

另一种方法就是通过 getMemoryPoolMXBeans() 获取所有memoryPoolMXBean,然后找到 Eden 区参数,解析它的设置。而在年代方面,只能通过 RuntimeMXBean.getInputArguments 获取,如果获取不到,那么就采用默认值 15,这样就能够手动触发 young gc 了。这是一种粗略的方式,并且能够保证一定能够达到效果(触发 young gc 次数不会少于自己想要触发的次数),在触发后再次通过 System.gc() 触发 full GC,来清理老年代不需要的对象,进而保证在开始提供服务时是一个干净的环境,模型都存在于老年代中,不会参与 young gc。

如何监控 JVM 运行状态:https://www.jianshu.com/p/978522f88ad0。这些知识在实现一个服务监控组件时可能会用到。

接下来,思考一下,希望以后能够在问题出现之前避免这种问题,而不是事后解决这个问题,所以在功能实现的时候需要清楚自己的对象是否会给 gc 带来麻烦,老的 JVM 并没有那么智能,有时候需要人为提供策略来协助它解决一些问题。

原文地址:https://www.cnblogs.com/43726581Gavin/p/9650778.html

时间: 2024-07-31 11:37:26

Java 代码监控 JVM 运行状态 —— 记一次 JVM 调优的毛招的相关文章

记一次数据库调优过程(IIS发过来SQLSERVER 的FETCH API_CURSOR语句是神马?)

记一次数据库调优过程(IIS发过来SQLSERVER 的FETCH API_CURSOR语句是神马?) 前几天帮客户优化一个数据库,那个数据库的大小是6G 这麽小的数据库按道理不会有太大的性能问题的,但是客户反应说CPU占用很高,经常达到80%~90% 我检查了任务管理器,确实是SQLSERVER占的CPU 而服务器的内存是16G内存,只占用了7G+ 客户的环境: Windows2008R2 SQLSERVER2005 SP3 64位 企业版 服务器内存:16G CPU:8核 RDS:阿里云主机

Linux下jetty报java.lang.OutOfMemoryError: PermGen space及Jetty内存配置调优解决方案

Linux下的jetty报java.lang.OutOfMemoryError: PermGen space及Jetty内存配置调优解决方案问题linux的jetty下发布程序后再启动jetty服务时,发现启动不了,从日志中找到报java.lang.OutOfMemoryError: PermGen space. 原因分析PermGen space,全称是Permanent Generation space,指的是内存3带中的永久区域.当java中间件启动时,会将相关的jar包和.class加载

46张PPT讲述JVM体系结构、GC算法和调优

本PPT从JVM体系结构概述.GC算法.Hotspot内存管理.Hotspot垃圾回收器.调优和监控工具六大方面进行讲述.(内嵌iframe,建议使用电脑浏览) 好东西当然要分享,PPT已上传至Github(点此下载),另外良心推荐阅读<深入理解Java虚拟机JVM高级特性与最佳实践.pdf>(点此下载).

JVM:垃圾回收机制和调优手段

转载请注明出处: jiq?钦's technical Blog - 季义钦 引言: 我们都知道JVM内存由几个部分组成:堆.方法区.栈.程序计数器.本地方法栈 JVM垃圾回收仅仅针对公共内存区域即:堆和方法区进行. 本文主要讨论两点,一是垃圾回收策略,二是调优的方法. 一.垃圾回收机制 1.1 分代管理 将堆和方法区按照对象不同年龄进行分代: u  堆中会频繁创建对象,基于一种分代的思想,按照对象存活时间将堆划分为新生代和旧生代两部分,我们不能一次垃圾回收新生代存活的对象就放入旧生代,而是要经过

Java代码中new对象的过程在jvm内存中的操作

1.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身 2.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中 3.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量

记一次apache调优

用apache支持的一台Django服务器,早上内存告警了 系统是centOS7 # uname -r3.10.0-229.4.2.el7.x86_64 # free -m              total        used        free      shared  buff/cache   availableMem:           7823        7172         270          21         380         382Swap: 

记一次SQL调优

insert优化 如果你在某一时刻有大量的insert操作,一条一条插入是非常耗时的.insert语句本身支持一次插入很多条记录,插入记录数上限受sql语句长度限制,一般一次插个几千条是没问题的.在我的 <如何手动实现Try Insert和Insert Or Update> 一文中对于各种情况都有具体的例子,这里就不赘述了. explain语句结果分析 SQL本身是一种对机器来说抽象级别很高的语言,我们通过SQL告诉DBMS我们需要什么,而没有告诉它具体要怎么做.DBMS会猜测性地以最优的方法

JVM 运行参数 &amp; 代码监控

1.Java代码监控 JDK提供java.lang.management包, 其实就是基于JMX技术规范,提供一套完整的MBean,动态获取JVM的运行时数据,达到监控JVM性能的目的. package com.agan.jvm; import java.lang.management.*; import java.util.Arrays; import java.util.List; public class JVMDemo { public static void main(String[]

Java架构师面试题——JVM性能调优

JVM内存调优 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数. 1.Full GC 会对整个堆进行整理,包括Young.Tenured和Perm.Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数. 2.导致Full GC的原因 1)年老代(Tenured)被写满 调优时尽量让对象在新生代GC时被回收.让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 . 2)持久代Pemanet Generati