关于运维:JAVA-OOM异常可观测最佳实践

35次阅读

共计 6508 个字符,预计需要花费 17 分钟才能阅读完成。

咱们常见 OOM 异样场景

堆溢出 – java.lang.OutOfMemoryError: Java heap space。

栈溢出 – java.lang.OutOfMemorryError。

栈溢出 – java.lang.StackOverFlowError。

元信息溢出 – java.lang.OutOfMemoryError: Metaspace。

间接内存溢出 – java.lang.OutOfMemoryError: Direct buffer memory。

GC 超限 – java.lang.OutOfMemoryError: GC overhead limit exceeded。

垃圾回收器

垃圾回收器就是内存回收的实践者,不同的产商、不同版本的虚拟机所蕴含的垃圾收集器都可能会有很大的差异,不同的虚拟机个别也都会提供各种参数供用户依据本人的利用特点和要求组合出各个内存分代所应用的收集器 ——《深刻了解 JAVA 虚拟机》

对于垃圾采集器(也叫垃圾回收器),在《深刻了解 JAVA 虚拟机》第三版目录中,曾经为咱们列举了大部分垃圾采集器。如下图所示:

查看本地 JVM 垃圾回收器

通过命令 java -XX:+PrintFlagsFinal -version |FINDSTR /i “:” 查看本地垃圾回收器为 Parallel。

C:\Users\lenovo>java -XX:+PrintFlagsFinal -version |FINDSTR /i “:”

 intx CICompilerCount                          := 4                                   {product}
uintx InitialHeapSize                          := 266338304                           {product}
uintx MaxHeapSize                              := 4257218560                          {product}
uintx MaxNewSize                               := 1418723328                          {product}
uintx MinHeapDeltaBytes                        := 524288                              {product}
uintx NewSize                                  := 88604672                            {product}
uintx OldSize                                  := 177733632                           {product}
 bool PrintFlagsFinal                          := true                                {product}
 bool UseCompressedClassPointers               := true                                {lp64_product}
 bool UseCompressedOops                        := true                                {lp64_product}
 bool UseLargePagesIndividualAllocation        := false                               {pd product}
 bool UseParallelGC                            := true                                {product}

java version “1.8.0_101”
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

查看 K8s 环境 JVM 垃圾回收器

在 K8s 环境中,个别应用 openjdk:8-jdk-alpine 或者 openjdk:8u292 作为根底镜像,启动服务后,并没有发现开启垃圾回收器。

root@ruoyi-system-c9c54dbd5-ltcvf:/data/app#
root@ruoyi-system-c9c54dbd5-ltcvf:/data/app# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=8388608 -XX:MaxHeapSize=134217728 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
openjdk version “1.8.0_292”
OpenJDK Runtime Environment (build 1.8.0_292-b10)
OpenJDK 64-Bit Server VM (build 25.292-b10, mixed mode)
root@ruoyi-system-c9c54dbd5-ltcvf:/data/app#

前置条件

1、jdk 版本为 1.8,也称为 JDK8。
每个 jdk 垃圾回收机制均不太一样,同样内存构造也产生了很大的变动,尤其是 1.6、1.7、1.8 三个版本体现出比拟显著,目前大部分企业用的是 jdk1.8 版本,本最佳实际也采纳 jdk1.8 版本作为根底,如果是其余版本的 jdk,能够借鉴思路。

2、接入 JVM 可观测。
请先接入 JVM 可观测,从观测云视图上咱们能够看出初始堆内存为 80 M , 与咱们启动时指定参数统一。

3、接入日志可观测
参考 Kubernetes 集群中日志采集的几种玩法,本次次要是采纳 socket 形式,也能够用其余形式。

堆溢出 -java.lang.OutOfMemoryError: Java heap space

堆溢出异样,置信大家很常见。即堆内对象不能进行回收了,堆内存继续增大,这样达到了堆内存的最大值,数据满了,所以就进去了。咱们间接放溢出的代码样例。设置启动最大堆内存为 -Xmx80m,这样咱们设置为最大堆内存,这样运行起来就很快就进去谬误了。

1、启动参数
-Xmx80m

-javaagent:C:/”Program Files”/datakit/data/dd-java-agent.jar

-Ddd.service=system

-Ddd.agent.port=9529

2、申请
浏览器申请 http://localhost:9201/exec/he…,须要等一段时间能力看到异样输入。看到异样输入后,即可返回观测云查看对应的日志。

3、观测云查看日志

栈溢出 -java.lang.OutOfMemorryError

抛出来的异样如下,如果真的须要创立线程,咱们须要调整帧栈的大小 -Xss512k,默认帧栈大小为 1M,如果设置小了,能够创立更多线程。如果帧栈不够用了,咱们须要理解什么中央创立了很多线程,线上程序须要用 jstack 命令,将以后线程的状态导出来放到文件里边,而后将文件上传到 fastthread.io 网站上进行剖析。若代码的确须要这么多的线程,此时能够依据【JVM 总内存 – 堆 = n*Java 虚拟机栈】,来减小堆的内存或者 Xss 来解决减少可调配线程的数量。

1、启动参数
-Xmx80m

-javaagent:C:/”Program Files”/datakit/data/dd-java-agent.jar

-Ddd.service=system

-Ddd.agent.port=9529

2、申请
浏览器拜访地址:http://localhost:9201/exec/st…

3、观测云查看日志
霎时创立线程,JVM 自带工具不在上报线程等相干监控指标,观测云依然上报最新 JVM 监控指标。

一段时间后,JVM 自带工具出现异常。

随后零碎就会呈现假死景象。

栈溢出 -java.lang.StackOverFlowError

次要体现在递归调用、死循环上,无论是因为栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无非调配的时候,HotSpot 虚拟机抛出的都是 StackOverFlowError 异样。程序每次递归的时候,程序会把数据后果压入栈,包含里边的指针等,这个时候就须要帧栈大一些能力接受住更多的递归调用。

1、启动参数
-Xmx80m

-javaagent:C:/”Program Files”/datakit/data/dd-java-agent.jar

-Ddd.service=system

-Ddd.agent.port=9529

2、申请
浏览器申请 http://localhost:9201/exec/st…

3、观测云查看日志

元信息溢出 -java.lang.OutOfMemoryError: Metaspace

在 JDK 8 当前,永恒代便齐全退出历史舞台,元空间作为其替代者退场,元数据区域也成为办法区。在默认设置下,很难迫使虚拟机产生办法区(元数据区域)的溢出异样,存储着类的相干信息,常量池,办法描述符,字段描述符,运行时产生大量的类就会造成这个区域的溢出。启动的时候设置 XX:MetaspaceSize 和 XX:MaxMetaspaceSize 过小时,间接启动报错。

1、启动参数
-Xmx80m

-XX:MetaspaceSize=30M

-XX:MaxMetaspaceSize=90M

-javaagent:C:/”Program Files”/datakit/data/dd-java-agent.jar

-Ddd.service=system

-Ddd.agent.port=9529

2、申请
浏览器输出 http://localhost:9201/exec/me…。

3、查看日志

元数据溢出后,不会往再写入日志等相干操作。

间接内存溢出 -java.lang.OutOfMemoryError: Direct buffer memory

间接内存溢出,咱们除了应用堆内存外,咱们还可能用间接内存,即堆外内存。NIO 为了进步性能,防止在 Java Heap 和 native Heap 中切换,所以应用间接内存,默认状况下,间接内存的大小和对内存大小统一。堆外内存不受 JVM 的限度,然而受制于机器整体内存的大小限度。如下代码设置堆最大内存为 80m,间接内存为 70m,而后咱们每次调配 1M 放到 list 里边。这个时候,当输入 70 次(Springboot 利用会小于 70 次)的时候,下次再调配的时候会报 nested exception is java.lang.OutOfMemoryError: Direct buffer memory。

1、启动参数
-Xmx80m

-javaagent:C:/”Program Files”/datakit/data/dd-java-agent.jar

-Ddd.service=system

-Ddd.agent.port=9529

2、申请
浏览器输出 http://localhost:9201/exec/di…。

3、观测云查看日志

GC 超限 -java.lang.OutOfMemoryError: GC overhead limit exceeded

后面三种都会引起 GC 超限。JDK1.6 之后新增了一个谬误类型,如果堆内存太小的时候会报这个谬误。如果 98% 的 GC 的时候回收不到 2% 的时候会报这个谬误,也就是最小最大内存呈现了问题的时候会报这个谬误。

观测云

无论是哪种异样,咱们能够在观测云 JVM 监控视图上找到一些线索,同时联合日志状况,对 JVM 参数进行调优。gc 次数过多过少、gc 工夫过长、线程忽然增多、堆内存忽然增多等等,都须要引起咱们关注。

观测云 OOM 日志告警
以上几种 OOM 异样场景也只是演示了如何产生异样以及在观测云上如何体现。理论生产过程中,OOM 异样会影响业务逻辑,更重大的会导致系统中断。能够借助观测云告警性能疾速告诉相干人员进行干涉。

配置 StackOverflowError 异样检测

配置 OutOfMemoryError 异样检测

配置告警告诉
监控器列表 – 分组,点击告警告诉按钮

配置告诉对象,观测云反对多种告诉对象,以后采纳的是邮件告诉。

触发异样后,能够收到邮件告诉,内容如下:

演示代码

本程序代码是在若依微服务框架上进行演示的。

package com.ruoyi.system.controller;

import com.ruoyi.common.core.domain.system.SysDept;
import com.ruoyi.common.core.web.domain.AjaxResult;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**

  • @author liurui
  • @date 2022/4/11 9:28
    */

@RequestMapping(“/exec”)
@RestController
public class ExceptionController {


@GetMapping("/heapOOM")
public AjaxResult heapOOM() {List<SysDept> list = new ArrayList<>();
    while (true) {
        try {TimeUnit.MILLISECONDS.sleep(1);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        list.add(new SysDept());
    }
}

@GetMapping("/stackOOM")
public AjaxResult stackOOM() {while (true) {Thread thread = new Thread(() -> {while (true) {
                try {TimeUnit.HOURS.sleep(1);
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }

        });
        thread.start();}
}

@GetMapping("/directBufferOOM")
public AjaxResult directBufferOOM() {
    final int _1M = 1024 * 1024 * 1;
    List<ByteBuffer> buffers = new ArrayList<>();
    int count = 1;
    while (true) {ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
        buffers.add(byteBuffer);
        System.out.println(count++);
    }
}

@GetMapping("/stackOFE")
public AjaxResult StackOFE() {stackOverFlowErrorMethod();
    return AjaxResult.success();}

public static void stackOverFlowErrorMethod() {stackOverFlowErrorMethod();
}

@GetMapping("/metaspaceOOM")
public AjaxResult metaspaceOOM() {while (true) {Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(SysDept.class);
        enhancer.setUseCache(false);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method,
                                    Object[] args, MethodProxy proxy) throws Throwable {return proxy.invokeSuper(obj, args);
            }
        });
        enhancer.create();}
}

}

正文完
 0