JMH和Arthas定位问题的案例分享

本文通过实例演示如何使用JMH进行性能测试对比,以及Arthas进行实时监控,揭示日志性能差异背后的调用链细节,助力解决线上性能问题。

点击上方“服务端思维”,选择“设为星标”

回复”669“获取独家整理的精选资料集

回复”加群“加入全国服务端高端社群「后端圈」

a2888cc354d2ace27677e21114ecb943.png

作者 | BryantChang

出品 | http://33h.co/kn6wx

最近的工作日并不算太平,各种大大小小的case和解case,发现已经有好久好久没有静下心来专心写点东西了。不过倒还是坚持利用业余时间学习了不少微课上的东西,发现大佬们总结的东西还是不一样,相比于大学时的那些枯燥的课本,大佬们总结出来的内容更活,更加容易理解。自己后面也会把大佬们的东西好好消化吸收,变成自己的东西用文字性的东西表达出来。

今天想总结的东西是最近工作中使用到的测试工具JMH以及Java运行时监控工具Arthas。他们在我的实际工作中也算是帮了大忙。所以在这里抛砖引玉一下这些工具的使用方法。同时也加深一下自己对这些工具的熟悉程度。对这两个工具,我都会首先简单介绍一下这些工具的大致使用场景,然后会使用一个在工作中真正遇到的性能问题排查为例详细讲解这两个工具的实际用法。话不多说,直奔主题。

问题描述

为了能够让我后面的实例能够贯穿这两个工具的使用,我首先简单描述下我们在开发中遇到的实际的性能问题。然后再引出这两个性能工具的实际使用,看我们如何使用这两个工具成功定位到性能瓶颈的。

问题如下:为了能够支持丢失率,我们将原先log4j2 的Async+自定义Appender的方式进行了修正,把异步的逻辑放到了自己改版后的Appender中。但我们发现修改后日志性能要比之前Async+自定义Appender的方式下降不少。这里由于隐私原因我并没有用实际公司中的实例,这里我用了一种其他同样能够体现问题的方式。我们暂时先不给出具体的配置文件,先给出性能测试代码和结果

代码

package com.bryantchang.appendertest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AppenderTest {

    private static final String LOGGER_NAME_DEFAULT = "defaultLogger";
    private static final String LOGGER_NAME_INCLUDE = "includeLocationLogger";
    private static final Logger LOGGER = LoggerFactory.getLogger(LOGGER_NAME_INCLUDE);
    public static final long BATCH = 10000;

    public static void main(String[] args) throws InterruptedException {
        while(true) {
            long start, end;
            start = System.currentTimeMillis();
            for (int i = 0; i < BATCH; i++) {
                LOGGER.info("msg is {}", i);
            }
            end = System.currentTimeMillis();
            System.out.println("duration of " + LOGGER_NAME_INCLUDE +  " is " + (end - start) + "ms");
            Thread.sleep(1000);
        }
    }
}

代码逻辑及其简单,就是调用logger.info每次打印10000条日志,然后记录耗时。两者的对比如下

2cbb3fbb6f90d360e336d2e6db5729c0.png

对比结果

86b6bac5c4eb58f653e5bff4907c9c4e.png

从这两张图片中我们能够看到同样的逻辑,两个程序的耗时差距相差了数十倍,但看图片,貌似仅仅是logger的名称不一样。对上面的实验结果进行分析,我们可能会有两个疑问

  • 上面的代码测试是否标准,规范

  • 如果真的是性能问题,那么这两个代码到底在哪个方法上有了这么大的差距导致了最终的性能差异

下面这两个工具就分别来回答这两个问题

JMH简介

第一个问题就是,测试的方法是否标准。我们在编写代码时用的是死循环+前后“掐秒表”的方式。假如我们要再加个多线程的测试,我们还需要搞一个线程池,除了代码本身的逻辑还要关心测试的逻辑。我们会想,有没有一款工具能够将我们从测试逻辑中彻底解放出来,只需要关心我们需要测试的代码逻辑。JMH就是这样一款Java的测试框架。下面是JMH的官方定义

JMH 是一个面向 Java 语言或者其他 Java 虚拟机语言的性能基准测试框架

这里面我们需要注意的是,JMH所测试的方法约简单越好,依赖越少越好,最适合的场景就是,测试两个集合put,get性能,例如ArrayList与LinkedList的对比等,这里我们需要测试的是批量打一批日志所需要的时间,也基本符合使用JMH的测试场景。下面是测试代码,bench框架代码以及主函数。

  • 待测试方法

public class LogBenchMarkWorker {

    private LogBenchMarkWorker() {}

    private static class LogBenchMarkWorkerInstance {
        private static final LogBenchMarkWorker instance = new LogBenchMarkWorker();
    }

    public static LogBenchMarkWorker getInstance() {
        return LogBenchMarkWorkerInstance.instance;
    }

    private static final String LOGGER_DEFAULT_NAME = "defaultLogger";
    private static final String LOGGER_INCLUDE_LOCATION = "includeLocationLogger";
    private static final Logger LOGGER = LoggerFactory.getLogger(LOGGER_DEFAULT_NAME);
    private static long BATCH_SIZE = 10000;

    public void logBatch() {
        for (int i = 0; i < BATCH_SIZE; i++) {
            LOGGER.info("msg is {}", i);
        }
    }
}

可以看到待测试方法非常简单,就是单批次一次性打印10000条日志的操作,所以并没有需要额外说明的部分。下面我们再来看benchmark部分。

public class LogBenchMarkMain {

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1)
    @Threads(1)
    public void testLog1() {
        LogBenchMarkWorker.getInstance().logBatch();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1)
    @Threads(4)
    public void testLog4() {
        LogBenchMarkWorker.getInstance().logBatch();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1)
    @Threads(8)
    public void testLog8() {
        LogBenchMarkWorker.getInstance().logBatch();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1)
    @Threads(16)
    public void testLog16() {
        LogBenchMarkWorker.getInstance().logBatch();
    }

在这段代码中,我们就会发现有了一些JMH中特有的东西,我下面进行简要介绍。

Benchmark注解:标识在某个具体方法上,表示这个方法将是一个被测试的最小方法,在JMH中成为一个OPS
BenmarkMode:测试类型,JMH提供了几种不同的Mode
    Throughput:整体吞吐量
    AverageTime:调用的平均时间,每次OPS执行的时间
    SampleTime:取样,给出不同比例的ops时间,例如99%的ops时间,99.99%的ops时间
fork:fork的次数,如果设置为2,JMH会fork出两个进程来测试
Threads:很容易理解,这个参数表示这个方法同时被多少个线程执行

在上面的代码中,我定义了4个待测试的方法,方法的Fork,BenchmarkMode均为测试单次OPS的平均时间,但4个方法的线程数不同。
除了这几个参数,还有几个参数,我会在main函数里面来讲,main代码如下所示

public class Main {
    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(LogBenchMarkMain.class.getSimpleName())
                .warmupIterations(5)
                .measurementIterations(5)
                .output("logs/BenchmarkCommon.log")
                .build();
        new Runner(options).run();
    }
}

我们可以看到,在main函数中,我们就是要设置JMH的基础配置,这里面的几个配置参数含义如下:

include:benchmark所在类的名字,可以使用正则表达
warmupIteration:预热的迭代次数,这里为什么要预热的原因是由于JIT的存在,随着代码的运行,会动态对代码的运行进行优化。因此在测试过程中需要先预热几轮,让代码运行稳定后再实际进行测试
measurementIterations:实际测试轮次
output:测试报告输出位置

我分别用两种logger运行一下测试,查看性能测试报告对比

使用普通logger

LogBenchMarkMain.testLog1   avgt    5  0.006 ± 0.001   s/op
LogBenchMarkMain.testLog16  avgt    5  0.062 ± 0.026   s/op
LogBenchMarkMain.testLog4   avgt    5  0.006 ± 0.002   s/op
LogBenchMarkMain.testLog8   avgt    5  0.040 ± 0.004   s/op

使用了INCLUDE_LOCATION的logger

LogBenchMarkMain.testLog1   avgt    5  0.379 ± 0.007   s/op
LogBenchMarkMain.testLog16  avgt    5  1.784 ± 0.670   s/op
LogBenchMarkMain.testLog4   avgt    5  0.378 ± 0.003   s/op
LogBenchMarkMain.testLog8   avgt    5  0.776 ± 0.070   s/op

这里我们看到,性能差距立现。使用INCLUDE_LOCATION的性能要明显低于使用普通logger的性能。这是我们一定很好奇,并且问一句“到底慢在哪”!!

Arthas 我的代码在运行时到底做了什么

Arthas是阿里开源的一款java调试神器,与greys类似,不过有着比greys更加强大的功能,例如,可以直接绘制java方法调用的火焰图等。这两个工具的原理都是使用了Java强大的字节码技术。毕竟我也不是JVM大佬,所以具体的实现细节没法展开,我们要做的就是站在巨人的肩膀上,接受并用熟练的使用好这些好用的性能监控工具。

实际操作

talk is cheap, show me your code,既然是工具,我们直接进行实际操作。我们在本机运行我们一开始的程序,然后讲解arthas的使用方法。

我们首先通过jps找到程序的进程号,然后直接通过arthas给到的as.sh对我们运行的程序进行字节码增强,然后启动arthas,命令如下

./as.sh pid

这个就是arthas的启动界面了,我们使用help命令查看工具所支持的功能

023cc259f486a950560992c63bb49afb.png

可以看到,arthas支持查看当前jvm的状态,查看当前的线程状态,监控某些方法的调用时间,trace,profile生成火焰图等,功能一应俱全
我们这里也只将几个比较常用的命令,其他的命令如果大家感兴趣可以详见官网arthas官网。这篇文章主要介绍下面几个功能

  • 反编译代码

  • 监控某个方法的调用

  • 查看某个方法的调用和返回值

  • trace某个方法

监控方法调用

这个主要的命令为monitor,根据官网的介绍,常用的使用方法为

monitor -c duration className methodName

其中duration代表每隔几秒展示一次统计结果,即单次的统计周期,className就是类的全限定名,methodname就是方法的名字,这里面我们查看的方法是Logger类的info方法,我们分别对使用两种不同logger的info方法。这里面的类是org.slf4j.Logger,方法时info,我们的监控语句为

monitor -c 5 org.slf4j.Logger info

监控结果如下

  • 使用普通appender

5c79dafd00c39f78f71b1d98c01ca7a9.png

  • 使用include appender

4e8ea47796b3f835d06f9f4484267570.png

我们可以看到,使用include appeder的打印日志方法要比普通的appender高出了3倍,这就不禁让我们有了疑问,究竟这两个方法各个步骤耗时如何呢。下面我们就介绍第二条命令,trace方法。

trace命令 & jad命令

这两个程序的log4j2配置文件如下

<?xml version="1.0" encoding="UTF-8"?>
<!--status:日志等级   monitorInterval:更新配置文件的时间间隔,单位秒-->
<configuration status="warn" monitorInterval="30">
    <!--定义所有的appender -->
    <appenders>
        <!--这个输出控制台的配置 -->
        <Console name="console" target="SYSTEM_OUT">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
            <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--日志打印格式 -->
            <PatternLayout pattern="[%d{HH:mm:ss.SSS}] [%-5p] %l - %m%n"/>
        </Console>


        <Async name="AsyncDefault" blocking="false" includeLocation="false">
            <AppenderRef ref="fileAppender"/>
        </Async>

        <Async name="AsyncIncludeLocation" blocking="false" includeLocation="true">
            <AppenderRef ref="fileAppender"/>
        </Async>

        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用 -->
        <!--append为TRUE表示消息增加到指定文件中,false表示消息覆盖指定的文件内容,默认值是true -->
        <File name="fileAppender" fileName="log/test.log" append="false">
            <PatternLayout pattern="[%d{HH:mm:ss.SSS}] [%-5p] %l - %m%n"/>
        </File>

        <!--添加过滤器ThresholdFilter,可以有选择的输出某个级别以上的类别  onMatch="ACCEPT" onMismatch="DENY"意思是匹配就接受,否则直接拒绝  -->
        <File name="ERROR" fileName="logs/error.log">
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="[%d{yyyy.MM.dd 'at' HH:mm:ss z}] [%-5p] %l - %m%n"/>
        </File>

        <!--这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
        <RollingFile name="rollingFile" fileName="logs/app.log"
                     filePattern="logs/$${date:yyyy-MM}/web-%d{yyyy-MM-dd}.log.gz">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%-5p] %l - %m%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
            </Policies>
            <DefaultRolloverStrategy>
                <Delete basePath="logs" maxDepth="2">
                    <IfFileName glob="*/*.log.gz" />
                    <IfLastModified age="7d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
    </appenders>

    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效 -->
    <loggers>
        <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
        <logger name="defaultLogger" additivity="false">
            <appender-ref ref="AsyncDefault"></appender-ref>
        </logger>

        <logger name="includeLocationLogger" additivity="false">
            <appender-ref ref="AsyncIncludeLocation"></appender-ref>
        </logger>

        <!--建立一个默认的root的logger -->
        <root level="INFO">

        </root>
    </loggers>

</configuration>

我们都是用了一个AsyncAppender套用了一个FileAppender。查看fileAppender,发现二者相同完全没区别,只有asyncAppender中的一个选项有区别,这就是includeLocation,一个是false,另一个是true。至于这个参数的含义,我们暂时不讨论,我们现在知道问题可能出在AsyncAppender里面,但是我们该如何验证呢。trace命令就有了大用场。

trace命令的基本用法与monitor类似,其中主要的一个参数是-n则是代表trace多少次的意思

trace -n trace_times className methodName

我在之前Log4j2的相关博客里面讲到过,任何一个appender,最核心的方法就是他的append方法。所以我们分别trace两个程序的append方法。

trace -n 5 org.apache.logging.log4j.core.appender.AsyncAppender append

trace结果如下

  • 使用普通appender

bcb96aef0fb560cc7153879b87d2e318.png

  • 使用include appender

94392153c738ccfdd29e3aa5e10860ed.png

我们立刻可以发现,两个trace的热点方法不一样,在使用include的appender中,耗时最长的方法时org.apache.logging.log4j.core.impl.Log4jLogEvent类中的createMemento方法,那么怎么才能知道这个方法到底做了啥呢,那就请出我们下一个常用命令jad,这个命令能够反编译出对应方法的代码。这里我们jad一下上面说的那个createMemento方法,命令很简单

jad org.apache.logging.log4j.core.impl.Log4jLogEvent createMemento

结果如下

6006fc5083e61ae203005c8b3483dd61.png

我们发现,这个方法中有个includeLocation参数,这个和我们的看到的两个appender的唯一不同的配置相吻合,我们此时应该有这个猜想,会不会就是这个参数导致的呢?为了验证这个猜想,我们引出下一个命令,watch

watch命令

watch命令能够观察到某个特定方法的入参,返回值等信息,我们使用这个命令查看一下这个createMemento方法的入参,如果两个程序的入参不同,那基本可以断定是这个原因引起命令如下

watch org.apache.logging.log4j.core.impl.Log4jLogEvent createMemento "params" -x 2 -n 5 -b -f

这里面的参数含义如下

-x 参数展开层次
-n 执行次数
-b 查看方法调用前状态
-f 方法调用后

其中的param代表查看方法的调用参数列表,还有其他的监控项详见官网官网

最终watch结果如下

  • 使用普通logger

1631a0428782d4042b53b1b8e41a2741.png

  • 使用include

9c0ba775a0c263bcd3e34c8c9432fd92.png

果不其然,这两个参数果然是一个true一个false,我们简单看下这个参数是如何传进来的,我们jad一下AsyncAppender的append方法

24e17b6d7528c9e86daa560bb0e132c3.png

我们发现这个includeLocation正是appender的一个属性,也就是我们xml中配置的那个属性。查看官网的相关分析,我们看到这个参数会使log的性能下降5–10倍

e1d7a44a7c31e66a337625d562ca01ba.png

不过为了一探究竟,我还是静态跟了一下这段代码

这个includeLocation会在event的createMemento中被用到,在序列化生成对象时会创建一个LogEventProxy,代码如下

public LogEventProxy(final LogEvent event, final boolean includeLocation) {
    this.loggerFQCN = event.getLoggerFqcn();
    this.marker = event.getMarker();
    this.level = event.getLevel();
    this.loggerName = event.getLoggerName();

    final Message msg = event.getMessage();
    this.message = msg instanceof ReusableMessage
            ? memento((ReusableMessage) msg)
            : msg;
    this.timeMillis = event.getTimeMillis();
    this.thrown = event.getThrown();
    this.thrownProxy = event.getThrownProxy();
    this.contextData = memento(event.getContextData());
    this.contextStack = event.getContextStack();
    this.source = includeLocation ? event.getSource() : null;
    this.threadId = event.getThreadId();
    this.threadName = event.getThreadName();
    this.threadPriority = event.getThreadPriority();
    this.isLocationRequired = includeLocation;
    this.isEndOfBatch = event.isEndOfBatch();
    this.nanoTime = event.getNanoTime();
}

如果includeLocation为true,那么就会调用getSource函数,跟进去查看,代码如下

public StackTraceElement getSource() {
        if (source != null) {
            return source;
        }
        if (loggerFqcn == null || !includeLocation) {
            return null;
        }
        source = Log4jLogEvent.calcLocation(loggerFqcn);
        return source;
    }
     public static StackTraceElement calcLocation(final String fqcnOfLogger) {
        if (fqcnOfLogger == null) {
            return null;
        }
        // LOG4J2-1029 new Throwable().getStackTrace is faster than Thread.currentThread().getStackTrace().
        final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
        StackTraceElement last = null;
        for (int i = stackTrace.length - 1; i > 0; i--) {
            final String className = stackTrace[i].getClassName();
            if (fqcnOfLogger.equals(className)) {
                return last;
            }
            last = stackTrace[i];
        }
        return null;
    }

我们看到他会从整个的调用栈中去寻找调用这个方法的代码行,其性能可想而知。我们用arthas监控一下,验证一下。

首先我们trace crateMemento方法

trace -n 5 org.apache.logging.log4j.core.impl.Log4jLogEvent createMemento

158a3fb0ae1c578dffe4d1fcd7606b3b.png

发现热点方法时org.apache.logging.log4j.core.impl.Log4jLogEvent的serialize(),继续trace下去

trace -n 5 org.apache.logging.log4j.core.impl.Log4jLogEvent serialize

9e7a3993273f8ba72423369ca3d5799c.png

看到热点是org.apache.logging.log4j.core.impl.Log4jLogEvent:LogEventProxy的构造方法,继续trace

trace -n 5 org.apache.logging.log4j.core.impl.Log4jLogEvent$LogEventProxy <init>

e6877eb13752af52ab0698bfb592d287.png

发现是getSource方法,继续

trace -n 5 trace -n 5 org.apache.logging.log4j.core.LogEvent getSource

58709925d8b59961720eed5fe9de42ac.png

热点终于定位到了,是org.apache.logging.log4j.core.impl.Log4jLogEvent的calcLocation函数,和我们静态跟踪的代码一样。

至此我们通过结合JMH和arthas共同定位出了一个线上的性能问题。不过我介绍的只是冰山一角,更多常用的命令还希望大家通过官网自己了解和实践,有了几次亲身实践之后,这个工具也就玩熟了。

— 本文结束 —

42366ae3afddc60e0ce18ca8de7861f7.gif

● 漫谈设计模式在 Spring 框架中的良好实践

● 颠覆微服务认知:深入思考微服务的七个主流观点

● 人人都是 API 设计者

● 一文讲透微服务下如何保证事务的一致性

● 要黑盒测试微服务内部服务间调用,我该如何实现?

e388293a378e3070b21c997abe9000dc.png

关注我,回复 「加群」 加入各种主题讨论群。

对「服务端思维」有期待,请在文末点个在看

喜欢这篇文章,欢迎转发、分享朋友圈

a94b1a9ed25ce2b95b8a1dcc7ec32397.png

在看点这里

70d3065e4a3abbe010bca05b93d8f89a.gif

基础篇-0-Java虚拟机导学课程 11:33 基础篇-1-初识JVM 22:27 基础篇-2-Java虚拟机的组成 04:47 基础篇-3-字节码文件的组成-以正确的姿势打开字节码文件 10:41 基础篇(补)-3.5-字节码文件的组成-基础信息 15:54 基础篇-4-字节码文件的组成-常量池方法 25:51 基础篇-5-字节码文件常见工具的使用1 11:43 基础篇-6-字节码文件常见工具的使用2 22:20 基础篇-7-类的生命周期加载阶段 22:09 基础篇-8-类的生命周期2连接阶段 19:58 基础篇-9-类的生命周期3初始化阶段 26:27 基础篇-10-类加载器的分类 13:56 基础篇-11-启动类加载器 13:36 基础篇-12-扩展应用程序类加载器 16:26 基础篇-13-双亲委派机制 18:43 基础篇-14-打破类的双亲委派机制-自定义类加载器 25:16 基础篇-15-打破双亲委派机制2-线程上下文类加载器 20:17 基础篇-16-打破双亲委派机制3-osgi类的热部署 11:53 基础篇-17-JDK9之后的类加载器 09:05 基础篇-18-运行时数据区-程序计数器 15:42 基础篇-19-栈-局部变量表 19:20 基础篇-20-栈-操作数栈帧数据 12:08 基础篇-21-栈-内存溢出 15:28 基础篇-22-堆内存 25:56 基础篇-23-方法区的实现 16:25 基础篇-24-方法区-字符串常量池 20:40 基础篇-25-直接内存 12:39 基础篇-26-自动垃圾回收 11:32 基础篇-27-方法区的回收 11:32 基础篇-28-引用计数法 15:41 基础篇-29-可达性分析法 20:25 基础篇-30-软引用 24:40 基础篇-31-弱虚终结器引用 12:08 基础篇-32-垃圾回收算法的评价标准 13:31 基础篇-33-垃圾回收算法1 10:05 基础篇-34-垃圾回收算法-分代GC 20:19 基础篇-35-垃圾回收器1 15:54 基础篇-36-垃圾回收器2 11:44 基础篇-37-垃圾回收器3 15:51 基础篇-38-g1垃圾回收器 26:23 实战篇-1-内存泄漏内存溢出 21:25 实战篇-2-解决内存泄漏-监控-top命令 12:16 实战篇-3-解决内存泄漏-监控-visualvm 12:50 实战篇-4-解决内存泄漏-监控-arthas tunnel 15:18 实战篇-5-解决内存泄漏-监控-prometheus-grafana 17:53 实战篇-6-解决内存泄漏-堆内存状况对比 08:39 实战篇-7-解决内存泄漏-内存泄漏产生的几大原因 16:01 实战篇-8-内存泄漏产生的原因2 13:30 实战篇-9-内存泄漏产生的原因3 10:43 实战篇-10-内存泄漏产生的原因4 10:04 实战篇-11-内存泄漏产生原因2-并发请求问题 17:30 实战篇-12-导出堆内存快照并使用MAT分析 08:38 实战篇-13-MAT内存泄漏检测原理 17:23 实战篇-14-服务器导出内存快照MAT使用小技巧 13:31 实战篇-15-实战1-查询大数据量导致的内存溢出 26:24 实战篇-16-实战2-mybatis导致的内存溢出 10:34 实战篇-17-实战3-k8s容器环境导出大文件内存溢出 26:13 实战篇-18-系统不处理业务时也占用大量的内存 14:13 实战篇-19-文章审核接口的内存问题 18:28 实战篇-20-btracearthas在线定位问题 20:15 实战篇-21-GC调优的核心目标 11:23 实战篇-22-GC调优的常用工具 12:05 实战篇-23-GC调优的常见工具2 14:25 实战篇-24-常见的GC模式 13:38 实战篇-25-基础JVM参数的设置 28:31 实战篇-26-垃圾回收器的选择 18:04 实战篇-27-垃圾回收参数调优 07:56 实战篇-28-实战-GC调优内存调优 30:43 实战篇-29-性能问题的现象解决思路 10:49 实战篇-30-定位进程CPU占用率高的问题 18:52 实战篇-31-接口响应时间很长问题的定位 14:44 实战篇-32-火焰图定位接口响应时间长的问题 12:03 实战篇-33-死锁问题的检测 14:37 实战篇-34-基准测试框架JMH的使用 28:24 实战篇-35-实战-性能调优 26:36 高级篇-01-GraalVM介绍 12:13 高级篇-02-GraalVM的两种运行模式 15:43 高级篇-03-使用SpringBoot3构建GraalVM应用 15:08 高级篇-04-将GraalVM应用部署到函数计算 25:13 高级篇-05-将GraalVM应用部署到Serverless 09:14 高级篇-06-参数优化故障诊断 22:31 高级篇-07-垃圾回收器的技术演进 13:09 高级篇-08-ShenandoahGC 22:50 高级篇-09-ZGC 14:35 高级篇-10-实战案例-内存不足时的垃圾回收测试 09:47 高级篇-11-JavaAgent技术 12:16 高级篇-12-JavaAgent环境搭建 15:24 高级篇-13-查看内存的使用情况 18:48 高级篇-14-生成内存快照 13:47 高级篇-15-获取类加载器的信息 16:26 高级篇-16-打印类的源码 18:00 高级篇-17-使用ASM增强方法 29:45 高级篇-18-使用ByteBuddy打印方法执行的参数耗时 21:55 高级篇-19-APM系统数据采集 24:30 原理篇-01-栈上的数据存储 15:05 原理篇-02-boolean在栈上的存储方式 22:48 原理篇-03-对象在堆上的存储1 17:27 原理篇-04-对象在堆上的存储2 25:14 原理篇-05-方法调用的原理1-静态绑定 19:26 原理篇-06-方法调用的原理2-动态绑定 15:25 原理篇-07-异常捕获的原理 12:00 原理篇-08-JIT即时编译器 14:49 原理篇-09-JIT即时编译器优化手段1-方法内联 16:49 原理篇-10-JIT即时编译器优化手段2-逃逸分析 09:03 原理篇-11-g1垃圾回收器原理-年轻代回收 27:57 原理篇-12-g1垃圾回收器原理-混合回收 17:24 原理篇-13-ZGC原理 26:27 原理篇-14-ShenandoahGC原理 09:39 面试篇-01-什么是JVM 16:38 面试篇-02-字节码文件的组成 15:02 面试篇-03-什么是运行时数据区 20:09 面试篇-04-哪些区域会出现内存溢出 11:56 面试篇-05-JDK6-8内存区域上的不同 14:36 面试篇-06-类的生命周期 17:17 面试篇-07-什么是类加载器 17:05 面试篇-08-什么是双亲委派机制 12:15 面试篇-09-如何打破双亲委派机制 18:10 面试篇-10-tomcat的自定义类加载器 31:18 面试篇-11-如何判断堆上的对象有没有被引用 10:05 面试篇-12-JVM中都有哪些引用类型 16:58 面试篇-13-theadlocal中为什么要使用弱引用 12:16 面试篇-14-有哪些垃圾回收算法 24:54 面试篇-15-有哪些常用的垃圾回收器 18:55 面试篇-16-如何解决内存泄漏问题 23:52 面试篇-17-常见的JVM参数 11:11 这是目前我学习的视频集合,要不要全看,或者少了什么,有哪些重要内容需要进行学习汇总或刷题或通过小实例验证
10-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值