第63天:Java OutofMemory错误的警报系统
#100daysofcode #devops #java #jvm

什么是OutofMemory

当没有足够的空间分配Java堆中的对象时,

java.lang.OutOfMemoryError异常。在这种情况下,垃圾收集器无法使空间可用以容纳新物体,并且不能进一步扩展堆。同样,当本机内存不足以支持Java类的加载时,可能会丢弃此错误。在极少数情况下,当花费大量时间进行垃圾收集并且很少的记忆被释放时,可能会抛出java.lang.OutOfMemoryError

当抛出java.lang.OutOfMemoryError异常时,还会打印堆栈跟踪。在该堆栈跟踪中,提到了原因,以便于解决问题。

时,将抛出此例外
  1. 记忆泄漏发生
  2. Not enough heap空间分配对象
  3. 如果GC一直在运行
  4. 试图分配比堆大小更大的内存
  5. 当Metaspace内存已满
  6. 当天然内存不足以分配

OOM如何发生

可能发生OOM的原因很多。 OutofMemoryError(OOME)很糟糕。它可以随时使用任何线程发生。除了退出程序,更改-XMX值并重新启动JVM外,您几乎无能为力。如果您将-XMX值太大,则会减慢应用程序。秘密是使最大堆值变正确的大小,既不小,也不太大。 oome可以使用任何线程发生,而当该线程发生时,该线程通常会停止。通常,没有足够的内存来构建OOME的堆栈跟踪,因此您甚至无法确定它发生的位置或为什么。

现在让我们进入警报系统代码。

public interface Listener {
    void alertMemoryLow(long used, long max, Map<Thread, StackTraceElement[]> allThreadStackTrace);
}

让我们定义一个Listener,该Listener将聆听警报。然后

 public static class OOMAlertService {
        private final List<Listener> listeners = new ArrayList<>();

        public OOMAlertService() {
            MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
            NotificationEmitter emitter = (NotificationEmitter) mbean;
            emitter.addNotificationListener((notification, o) -> {
                if (notification.getType().equals(
                        MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED)) {
                    long maxMemory = tenuredGenPool.getUsage().getMax();
                    long usedMemory = tenuredGenPool.getUsage().getUsed();
                    for (Listener listener : listeners) {
                        listener.alertMemoryLow(usedMemory, maxMemory, Thread.getAllStackTrace());
                    }
                }
            }, null, null);
        }

        public boolean addListener(Listener listener) {
            return listeners.add(listener);
        }

        public boolean removeListener(Listener listener) {
            return listeners.remove(listener);
        }

        private static final MemoryPoolMXBean tenuredGenPool = findTenuredGenPool();

        public static void setUsageThreshold(double threshold) {
            if (threshold <= 0.0 || threshold > 1.0) {
                throw new IllegalArgumentException("Threshold Percentage outside range");
            }
            long maxMemory = tenuredGenPool.getUsage().getMax();
            long warningThreshold = (long) (maxMemory * threshold);
            tenuredGenPool.setUsageThreshold(warningThreshold);
        }


        private static MemoryPoolMXBean findTenuredGenPool() {
            for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
                if (pool.getName().contains("Old")
                        && pool.getType() == MemoryType.HEAP
                        && pool.isUsageThresholdSupported()) {
                    return pool;
                }
            }
            throw new Exception("Could not find tenured space");
        }
    }

在我们的oomalterservice中,我们添加了侦听器来实现oomalertservice.listener界面,使用一种方法arpermemorylow( long long long max),当阈值为阈值时将被称为到达。

一旦我们将MemoryMXBean降低到NotificationEmitter,我们就可以向MemoryMXBean添加NotificationListener。您应该验证通知是否为MEMORY_THRESHOLD_EXCEEDED类型。

此通知将快速发射。要注意的是,听众是由一个称为Low Memory Detector thread的特殊线程调用的,是标准JVM的一部分。

什么是阈值?我们应该监视哪个池中的哪个?唯一明智的游泳池是终身世代(旧空间)。当您使用-Xmx256m设置内存的大小时,您将设置在终身生成中使用的最大内存。在findTenuredGenPool()方法中进行搜索,然后返回第一个类型堆以获取终身内存。

setUsageThreshold( double threshold)方法中,我指定何时要通知。该阈值是一个全球设置。因此,每个Java虚拟机只有一个用法阈值。阈值值用于根据固定生成池的最大存储器大小(not the Runtime.getRuntime().maxMemory() value!)的最大存储器大小来计算使用阈值。

findTenuredGenPool()中,我试图找到堆内存的Tenured / Old gen内存。首先,让我们了解JVM堆和非堆内存。

堆内存

堆内存是Java VM从中为所有类实例和数组分配内存的运行时数据区域。堆可能是固定或可变尺寸。垃圾收集器是一种自动内存管理系统,可为对象收回堆内存。

  1. 伊甸园空间:最初为大多数对象分配内存的池。
  2. 幸存者空间:包含在伊甸园垃圾收藏中幸存的物体的游泳池。
  3. 占用世代或旧gen:池中包含在幸存者空间中存在一段时间的物体。

非主内存

非主存储器包括在所有线程之间共享的方法区域以及内部处理或优化Java VM所需的内存。它存储了每个类结构,例如运行时常数池,字段和方法数据以及方法和构造函数的代码。该方法区域在逻辑上是堆的一部分,但是根据实现,Java VM可能不会垃圾收集或压实它。像堆内存一样,方法区域可能具有固定或可变大小。方法区域的内存不需要连续。

  1. 永久生成:包含虚拟机身的所有反射数据的池,例如类和方法对象。使用使用类数据共享的Java VM,这一代人分为只读和读写区域。
  2. 代码缓存:热点Java VM还包括一个代码缓存,包含用于编译和存储本机代码的内存。

How can I monitor memory usage of my Tomcat/JVM?

对于应用程序,下图将使您了解如何分发内存。

JVM Memory

所以现在让我们回到方法findTenuredGenPool()

private static MemoryPoolMXBean findTenuredGenPool() {
            for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
                if (pool.getName().contains("Old")
                        && pool.getType() == MemoryType.HEAP
                        && pool.isUsageThresholdSupported()) {
                    return pool;
                }
            }
            throw new Exception("Could not find tenured space");
        }
    }

在这种方法中,我们正在寻找一个类型堆,也以其名称的池包含旧词。这是我们可以找到终身代池的一种方式。

因此,使用此池大小及其阈值,我们可以制作警报服务,这将在内存较低时为我们提供警报。这样,我们可以采取必要的步骤来防止OOM错误。

所以现在运行此服务

class Day63 {

    public static void main(String[] args) throws Exception {
        OOMAlertService.setUsageThreshold(60.0/100.0);

        OOMAlertService mws = new OOMAlertService();
        mws.addListener((usedMemory, maxMemory, stacktrace) -> {
            System.out.println("Memory usage low!!!");
            double percentageUsed = ((double) usedMemory) / maxMemory;
            System.out.println("percentageUsed = " + percentageUsed);
            OOMAlertService.setUsageThreshold(80.0/100.0);
            stacktrace.forEach((key, value) -> System.out.println(key + " " + Arrays.toString(value)));
        });

        List<List<Object>> numbers = new ArrayList<>();
        while (true) {
            numbers.add(new ArrayList<>());
        }
    }

在这里,我只是创建一个无限地诱导OOM的对象列表。