JVM(jdk1.8) 实战

发布于:2025-09-13 ⋅ 阅读:(14) ⋅ 点赞:(0)


Arthas 官网

内存泄漏和内存溢出

解决内存泄漏

发现问题

监控 top 命令



load average: 0.0.2, 0.10, 0.06:过去的1、5、15分钟系统负载率为 2%、10%、6% 
3948748 total:代表系统总内存,单位为 KB 
3280784 free:代表系统剩余内存,单位为 KB 
used:当前使用的内存 
buff/cache:系统缓存
PID:进程ID
VIRT:虚拟内存(不用过多关注)
RES:常驻内存(重点关注,基本代表该进程使用了多少内存)
SHR:共享内存(操作系统只加载一次,多进程共享,由于常驻内存包含了共享内存,所有计算进程使用的内存需要用常驻内存减去共享内存)
&CPU:cpu使用率,1.0 代表cpu有 1%的使用时间
%MEM:进程占用系统实际的物理内存
TIME+:进程启动以来所累计消耗cpu的时间
COMMAND:进程启动命令

该命令只能看在系统的大致情况,无法查看JVM中的内存情况,命令适合做初步判断

Visualvm

使用 Visualvm 远程连接
禁止线上环境 使用 Visualvm 连接(可能导致不稳定),建议在测试环境来定位问题

运行 jar 包的时候开启 Visualvm 远程连接功能,比如:

java -Djava.rmi.server.hostname=<你的服务器IP地址> \
     -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=<指定的端口号> \
     -Dcom.sun.management.jmxremote.ssl=false \
     -Dcom.sun.management.jmxremote.authenticate=false \
     -jar your-app.jar

    -Dcom.sun.management.jmxremote.ssl=false :不使用 SSL
    -Dcom.sun.management.jmxremote.authenticate=false :不需要认证

Arthas


 

Arthas Tunnel | arthas (aliyun.com) 文档,下载好 arthas-tunnel-server 服务后就可以启动了

java -jar -Darthas.enable-detail-pages=true arthas-tunnel-server-4.0.2-fatjar.jar

-Darthas.enable-detail-pages=true 是开启Web页面

<!-- Arthas 依赖 -->
<dependency>
    <groupId>com.taobao.arthas</groupId>
    <artifactId>arthas-spring-boot-starter</artifactId>
    <version>3.6.7</version>
</dependency>

# arthas 配置
arthas:
  app-name: ${spring.application.name} # 应用名称
  tunnel-server: ws://127.0.0.1:7777/ws # tunnelServer地址,默认7777端口
#  http-port: 8080 # http访问端口  # 浏览器访问 Arthas 的 Web 界面端口,不配置默认8080
#  telnet-port: 9999 # Telnet 远程连接 Arthas 并执行命令 的端口,不配做默认9999

启动tunnel服务之后就可以在 127.0.0.1:8080/apps.html 看到所有连接的 springboot 程序了

这样就可以在浏览器上同时管理多个SpringBoot 程序了

Prometheus + Grafana

在 SpringBoot 将 JVM 参数通过HTTP 的形式暴露出去:

        <!-- 监控 springboot 程序依赖-actuator  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- Prometheus监控依赖,可以将 actuator 的指标数据转换为 Prometheus 的格式,实现可视化监控 -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <scope>runtime</scope>
        </dependency>
management:
  endpoints:
    web:
      exposure:
        include: "*"   # 开发所有端点。这意味着所有管理端点都将通过 HTTP 暴露出来,方便进行监控和管理。
  endpoint:
    metrics:
      enabled: true    # 启用指标端点。这允许收集和暴露应用程序的各种指标数据。
  metrics:
    export:
      prometheus:
        enabled: true  # 启用 Prometheus 指标导出。这使得应用程序的指标数据可以被 Prometheus 监控系统抓取和分析。
    tags:
      application: ${spring.application.name}  # 应用名称

启动后就可以在 /actuator 或者 /actuator/prometheus 接口中查看 jvm 参数了

注:两者框架都可以监控 jvm ,micrometer-registry-prometheus 可以可视化监控指标,但是监控不全面,没有健康检查、日志、线程 等监控信息。而 spring-boot-starter-actuator 监控全面但是不能可视化监控指标。两者结合,相得益彰。

堆内存状况对比

可以观察多次FGC之后内存占比有没有增加

也可以通过采样器来观察GC之后又哪些对象占比多少,是否一直存在.

代码中的内存泄露

equals() 和 hashCode() 导致

当向 HashMap put 一个数据时,HashMap 会对 Key 先调用 hashCode() 方法来判断哈希冲突,如果哈希冲突就会调用 equals() 判断是否相同,不相同就尾插法道数组中(或者红黑树),相同就不能插入(key相同)。
如果类没有重写 equals() 和 hashCode() 方法,那么在调用的时候就会使用父类 Object 的 equals() 和 hashCode() 方法。Object 类的 hashCode() 方法为每个对象返回一个唯一的哈希码(通常是基于对象的内存地址),而 Object 类的 equals() 方法仅当两个对象引用指向同一个对象时才返回 true。

内部类引用外部类

反编译之后看到内名内部类是会传入调用者对象作为赋值,一直持有改对象

ThreadLocal 的使用

String 的 intern 方法

intern 方法是将字符串存到字符串常量池,这个在 JDK8(8的常量池在堆中) 中依然会内存溢出

静态字段保存对象

@Lazy :懒加载注解,Spring 容器启动的时候并不会马上去加载这个Bean,而是当这个类被使用的时候才去加载到 IOC 中

资源没有正常关闭

并发请求导致内存溢出

这个问题导致内存溢出有一般满足3中情况:
1、单次请求的数据量较大
2、并发量超过了200(springboot 中 tomcat 默认线程并发数是200)
3、单次处理时间比较长

这里可以使用 Apifox 一体化工具测试来并发测试发现内存溢出问题

问题诊断

MemoryAnalyzer 诊断工具

可以设置如下参数来测试内存溢出生成的快照文件:

-Xms256m 
-Xmx256m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\\java\\study\\springboot\\src\\main\\resources\\jvm\\memory.hprof

当内存溢出时会生成一个内存快照 .hprof 文件,可以使用 MemoryAnalyzer (MAT)这种 jvm 内存分析工具打开改该文件,根据展示的信息可以定位到问题在哪。

也可以简单的将 .hprof 文件 拖到 IDEA 中 也可以定位问题

MAT内存泄露检查原理

        <!-- jol依赖,用于打印对象内存对象布局,用于排查内存泄漏 -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.17</version>
        </dependency>

System.out.println(ClassLayout.parseClass(String.class).toPrintable());

这里显示 String类 中内容分布:
(object header: mark/class):类的基本信息,一共12字节
char[] String.value:String 类中的数组,4个字节保存对char[] 的引用
object alignment gap:对象对齐填充,我的是操作系统是64位,所有需要能被8整除,所以补4个字节,24能整除8

服务器导出内存快照和MAT使用小技巧

修复问题-实战解决

查询大量数据导致的内存溢出

mybatis 导致的内存溢出

MyBatis 在处理 <foreach> 标签时会遍历集合或数组,会存到map中,key是参数名称,值是List<值>。Java 的字符串拼接机制可能会导致大量的临时字符串对象被创建。

K8s容器环境大文件导致大文件内存溢出

由于没有学习过 K8S,所以 .....

系统不处理业务时也占用大量内存

文章内存审核接口内存问题*

然后设置线程池参数,如果最大线程数设置过大,可能导致大量线程任务保持在内存中;设置过小,可能导致其他进入的线程报错,因此线程池参数是一个难点。如果断电或者重启会丢失任务,所以使用线程池做文章审核不是一个很好的选择

btrace和arthas在线定位问题

使用 jamp -histo:live 进程ID > 1.txt 命令保存的直方图文本中可以看到我们编写的类中UserEntity占比最多,很可能是这个类内存溢出

然后再 Arthas 的 stack 命令就可以出打印运行过程中 userEntity 类在带代码哪个地方调用

也可以使用 btrace 工具打印出具体的堆栈信息

下载好之后在配置环境变量 :D:\app\Btrace\btrace-v2.2.6-bin\bin

创建一个maven工程编写 brtace 脚本:

添加依赖:(就是 D:\app\Btrace\btrace-v2.2.6-bin\libs 目录下的3个jar包)

        <dependency>
            <groupId>org.openjdk.btrace</groupId>
            <artifactId>btrace-agent</artifactId>
            <version>${btrace.version}</version>
            <scope>system</scope>
            <systemPath>D:\app\Btrace\btrace-v2.2.6-bin\libs\btrace-agent.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.openjdk.btrace</groupId>
            <artifactId>btrace-boot</artifactId>
            <version>${btrace.version}</version>
            <scope>system</scope>
            <systemPath>D:\app\Btrace\btrace-v2.2.6-bin\libs\btrace-boot.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.openjdk.btrace</groupId>
            <artifactId>btrace-client</artifactId>
            <version>${btrace.version}</version>
            <scope>system</scope>
            <systemPath>D:\app\Btrace\btrace-v2.2.6-bin\libs\btrace-client.jar</systemPath>
        </dependency>

在 IDEA中 pom 文件爆红是正常现象

编写代码:

@BTrace
public class BtraceScript {
    /**
     * 打印com.boot.controller.jvm类所有方法的堆栈跟踪
     */
    @OnMethod(clazz="com.boot.controller.JvmController", method="/.*/")
    public static void jvmControllerTack() {
        jstack(); // 打印当前线程的堆栈跟踪
    }
}

Linux执行命令:ps -ef|grep java 查看程序的进程id,执行命令打印对应的堆栈信息:(未成功)

btrace 9652 D:\app\Btrace\brtace-script-demo\src\main\java\com\zwf\BtraceScript.java 

GC调优

核心目标

常用工具


输出目录为xxx.log

https://gceasy.io/ 这是一个收费的一个在线诊断GC日志的网站

常见的GC模式

解决GC调优的手段

优化JVM参数

oracle官网查看JVM所有的参数设置

预留500MB内存给突发情况

如果不知道设置多少,可以不设置(设置有更好)


JDK9之后可以直接将后3行的日志输出替换为一行

更换垃圾回收器

性能调优

性能问题的现象和解决思路

jstack命令和visualvm界面都可以生成线程转储文件

定位进程CPU占有率高问题

在线转储可视化网站               

定位接口响应慢问题


arthas底层通过动态代理方式增强,使用完后一定要stop结束监控,不然会损耗性能

火焰图定位接口响应慢问题

死锁的检查


推荐使用解决方案3

基准测试框架JMH

mvn archetype:generate ^
-DinteractiveMode=false ^
-DarchetypeGroupId="org.openjdk.jmh" ^
-DarchetypeArtifactId="jmh-java-benchmark-archetype" ^
-DgroupId="org.sample" ^
-DartifactId="test" ^
-Dversion="1.0"

使用maven 拉取JMH项目代码脚手架,然后修改pom文件的中依赖版本

编写 MyBenchmark.java

// 预热5次,每次1秒
@Warmup(iterations = 5, time = 1)
// 启动线程数
@Fork(value = 2)
// 显示平均时间
@BenchmarkMode(Mode.AverageTime)
// 输出时间单位为纳秒
@OutputTimeUnit(TimeUnit.NANOSECONDS)
// 变量共享范围
@State(Scope.Benchmark) // 在测试环境中全部共享
public class MyBenchmark {
    @Benchmark
    public int testMethod() {
        int i = 0;
        i++;
        return i;
    }
}

然后执行maven生命周期中的verify,对集成测试的结果进行检查并生成 benchmarks.jar包

然后执行 java -jar benchmarks.jar 就能对 testMethod 方法预热测试了

也可以直接在 MyBenchmark.java 中使用 main 方法启动来对方法进行预热测试:但是这样idea对结果造成轻微的误差

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(MyBenchmark.class.getSimpleName())
                .forks(1)
//                .resultFormat(ResultFormatType.JSON) // 输出结果JSON文件           
                .build();
        new Runner(options).run();
    }

然后将文件放到网站: JMH 结果文件可视化平台

发现使用 LocalDateTime + 共享对象 来格式化时间是性能最高的