首页 > 其他分享 >OOM实战演练

OOM实战演练

时间:2024-01-31 18:35:29浏览次数:19  
标签:实战 Java JDK OOM 虚拟机 public 内存 演练 溢出

1. 实战:OutOfMemoryError异常

  • 在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能,并且将初步介绍若干最基本的与自动内存管理子系统相关的HotSpot虚拟机参数。
  • 本节实战的目的有两个:
    • 第一,通过代码验证《Java虚拟机规范》中描述的各个运行时区域储存的内容;
    • 第二,在工作中遇到实际的内存溢出异常时,能根据异常的提示信息迅速得知是哪个区域的内存溢出,知道怎样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理。
  • 代码清单开头都注释了执行时需要设置的虚拟机启动参数(注释中VM Args后面跟着的参数),这些参数对实验的结果有直接影响,请调试代码的时候不要忽略掉。如果读者使用控制台命令来执行程序,那直接跟在Java命令之后书写就可以。
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

1.1 Java堆溢出

  • Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
  • 限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析。
import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 *
 * @author zzm
 */
public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();

        while (true) {
            list.add(new OOMObject());
        }
    }
}

image

  • Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息java.lang.OutOfMemoryError会跟随进一步提示Java heap space
    • 要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具对Dump出来的堆转储快照进行分析。第一步首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(MemoryOverflow)。
    • 如果是内存泄漏,可进一步通过工具査看泄漏对象到GCRoots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。

用 IDEA Profiler 打开 java_pidxxx.hprof文件

image

  • 如果不是内存泄漏,那么内存中的对象确实都是必须存活的,那就应当检査Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

1.2 虚拟机栈和本地方法栈溢出

  • 由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说:

    • -Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的
    • 容量只能由-Xss参数来设定。
  • 关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:

    1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
    2. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请足够的内存时,将抛出OutOfMemoryError异常。
  • 《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。

    为了验证这点,做两个实验,先将实验范围限制在单线程中操作,尝试下面两种行为是否能让HotSpot虚拟机产生OutOfMemoryError异常:

    1. 使用-Xss参数减少栈内存容量。

      结果:抛出StackOverfowError异常,异常出现时输出的堆深度相应缩小。

    2. 定义了大量的本地变量,增大此方法帧中本地变量表的长度。
      结果:抛出StackOverlowError异常,异常出现时输出的堆栈深度相应缩小。



/**
 * VM Args:-Xss190k
 *
 * @author zzm
 */
public class JavaVMStackSOF_1 {

    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF_1 oom = new JavaVMStackSOF_1();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

image

对于不同版本的Java虚拟机和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操作系统内存分页大小。譬如上述方法中的参数-Xss128k可以正常用于32位Wndows系统下的JDK6,但是如果用于64位Windows系统下的JDK 11,则会提示栈容量最小不能低于180K,而在Linux下这个值则可能是228K,如果低于这个最小限制,HotSpot虚拟器启动时会给出如下提示:

image



/**
 * VM: JDK 1.0.2, Sun Classic VM
 *
 * @author zzm
 */
public class JavaVMStackSOF_3 {
    private static int stackLength = 0;
    
    public static void test() {
        long unused1, unused2, unused3, unused4, unused5,
                unused6, unused7, unused8, unused9, unused10,
                unused11, unused12, unused13, unused14, unused15,
                unused16, unused17, unused18, unused19, unused20,
                unused21, unused22, unused23, unused24, unused25,
                unused26, unused27, unused28, unused29, unused30,
                unused31, unused32, unused33, unused34, unused35,
                unused36, unused37, unused38, unused39, unused40,
                unused41, unused42, unused43, unused44, unused45,
                unused46, unused47, unused48, unused49, unused50,
                unused51, unused52, unused53, unused54, unused55,
                unused56, unused57, unused58, unused59, unused60,
                unused61, unused62, unused63, unused64, unused65,
                unused66, unused67, unused68, unused69, unused70,
                unused71, unused72, unused73, unused74, unused75,
                unused76, unused77, unused78, unused79, unused80,
                unused81, unused82, unused83, unused84, unused85,
                unused86, unused87, unused88, unused89, unused90,
                unused91, unused92, unused93, unused94, unused95,
                unused96, unused97, unused98, unused99, unused100;
        
        stackLength++;
        test();
        
        unused1 = unused2 = unused3 = unused4 = unused5 =
        unused6 = unused7 = unused8 = unused9 = unused10 =
        unused11 = unused12 = unused13 = unused14 = unused15 =
        unused16 = unused17 = unused18 = unused19 = unused20 =
        unused21 = unused22 = unused23 = unused24 = unused25 =
        unused26 = unused27 = unused28 = unused29 = unused30 =
        unused31 = unused32 = unused33 = unused34 = unused35 =
        unused36 = unused37 = unused38 = unused39 = unused40 =
        unused41 = unused42 = unused43 = unused44 = unused45 =
        unused46 = unused47 = unused48 = unused49 = unused50 =
        unused51 = unused52 = unused53 = unused54 = unused55 =
        unused56 = unused57 = unused58 = unused59 = unused60 =
        unused61 = unused62 = unused63 = unused64 = unused65 =
        unused66 = unused67 = unused68 = unused69 = unused70 =
        unused71 = unused72 = unused73 = unused74 = unused75 =
        unused76 = unused77 = unused78 = unused79 = unused80 =
        unused81 = unused82 = unused83 = unused84 = unused85 =
        unused86 = unused87 = unused88 = unused89 = unused90 =
        unused91 = unused92 = unused93 = unused94 = unused95 =
        unused96 = unused97 = unused98 = unused99 = unused100 = 0;
    }
    
    public static void main(String[] args) {
        try {
            test();
        } catch (Error e) {
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}

image

对于不同版本的Java虚拟机和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操作系统内存分页大小。譬如上述方法中的参数-Xss128k可以正常用于32位Windows系统下的JDK6,但是如果用于64位Windows系统下的JDK 11,则会提示栈容量最小不能低于180K,而在Linux下这个值则可能是228K,如果低于这个最小限制,HotSpot虚拟器启动时会给出如下提示:

image


实验结果表明: 无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverfowError异常。可是如果在允许动态扩展栈容量大小的虚拟机上,相同代码则会导致不一样的情况。

image

  • 可见相同的代码在Classic虚拟机中成功产生了OutOfMemoryError而不是StackOverFlowError异常。

  • 如果测试时不限于单线程,通过不断建立线程的方式,在HotSpot上也是可以产生内存溢出异常的。但是这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。甚至可以说,在这种情况下,给每个线程的分配的内存越大,反而越容易产生内存溢出异常。
  • 原因其实不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆方法区这两部分的内存的最大值,
    • 那剩余的内存即为2GB(操作系统限制)减去最大堆容量,再减去最大方法区容量,由于程序计数器消耗内存很小,可以忽略掉
    • 如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存就由虚拟机栈和本地方法栈来分配了。
  • 因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。

image


/**
 * VM Args:-Xss2M (这时候不妨设大些,请在32位系统下运行)
 *
 * @author zzm
 */
public class JavaVMStackOOM {

    private void dontStop() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

出现StackOverflowError异常时,会有明确错误堆栈可供分析,相对而言比较容易定位到问题所在。如果使用HotSpot虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说大多数情况下)到达1000~2000是完全没有问题,对于正常的方法调用(包括不能做尾递归优化的递归调用),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。这种通过减少内存的手段来解决内存溢出的方式,如果没有这方面处理经验,一般比较难以想到,在开发32位系统的多线程应用时注意。也是由于这种问题较为隐蔽,从JDK 7起,以上提示信息中“unable to create native thread”后面,虚拟机会特别注明原因可能是"possibly out of memory or process/resource limits reached".

1.3. 方法区和运行时常量池溢出

  • 由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。HotSpot从JDK 7开始逐步去永久代的计划,并在JDK 8中完全使用元空间来代替永久代

  • String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

  • 在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize

    -XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。

    注意: 如果要测试永久代 需要把JDK设置成1.6版本

​ 1. JDK 1.6运行结果

image

  • 从运行结果中可以看到,运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是PermGen space,说明运行时常量池的确是属于方法区(即JDK 6的HotSpot虚拟机中的永久代)的一部分。

    1. JDK 1.8运行结果

image

  • 使用JDK 7或更高版本的JDK来运行这段程序并不会得到相同的结果,
    • 无论是在JDK 7中继续使用-XX:MaxPermSize参数
    • 或者在JDK8及以上版本使用-XX:MaxMeta-spaceSize参数把方法区容量同样限制在6MB,也都不会重现JDK 6中的溢出异常,循环将一直进行下去,永不停歇!。
  • 出现这种变化,是因为自 JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,所以在JDK 7及以上版本,限制方法区的容量对该测试用例来说是毫无意义的。这时候使用-Xmx参数限制最大堆到6MB就能够看到以下两种运行结果之一,具体取决于哪里的对象分配时产生了溢出:

image

image

方法区演变历程

image


public class RuntimeConstantPoolOOM_2 {

    public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}

image

  • 这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。产生差异的原因是:

    • 在JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。

image

  • 在JDK7的intern()方法就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,只需要在常量池里记录首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对比str2比较返回false,这是因为java这个字符串在执行StringBuilder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求首次遇到的原则,计算机软件这个字符串则是首次出现的,因此结果返回true。

image


方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

  • 方法区的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出为止。虽然直接使用JavaSE API也可以动态产生类,但在操作起来比较麻烦。借助了CGLib直接操作字节码运行时生成了大量的动态类。

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 *
 * @author zzm
 */
public class JavaMethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }

    static class OOMObject {
    }
}

image

  • 方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,要达成的条件是比较苛刻的。在经常运行时生成大量动态类的应用场景,就应该特别关注这些类的回收状况。这类场景除了之前提到的程序使用了CGLib字节码增强和动态语言外,常见的还有: 大量JSP或动态产生JSP文件的应用、基于OSGi的应用等。
  • 在JDK 8以后,永久代便完全退出了历史舞台,元空间作为其替代者场。在默认设置下,前面列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常了。不过为了让使用者有预防实际应用里出现破坏性的操作,HotSpot还是提供了一些参数作为元空间的防御措施,
    • -XX:MaxMetaspaceSize: 设置元空间最大值,默认是-1,即不限制或者说只受限于本地内存大小。
    • -XX:MetaspaceSize: 指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整,如果释放了大量的空间,就适当降低该值。如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
    • -XX:MinMetaspaceFreeRatio: 作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:MaxMetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。

1.4. 本机直接内存溢出

  • 直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致,通过反射获取Unsafe实例进行内存分配,Unsafe类的getUnsafe方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在 JDK10时才将Unsafe的部分功能通过varHandle开放给外部使用,因为使用DirectByteBufer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe:allocateMemory()

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 *
 * @author zzm
 */
public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

image

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检査一下直接内存方面的原因了。

标签:实战,Java,JDK,OOM,虚拟机,public,内存,演练,溢出
From: https://www.cnblogs.com/ccblblog/p/17999878

相关文章

  • 12.分组并发压测实战
    1.压测计划制定压测策略不同的并发数10,50,100,200,……持续时间30s记录结果测试期望结果验证能够支撑多大并发数,峰值数验证错误率,定义可接受范围,<=0.1%or<=0.5%ormust=0%2.压测策略通过对比并发数与流量还有错误率的关系,找到一个最合理的系统可支撑最......
  • ChatGPT全栈开发实战:从需求分析到数据可视化,一站式指南助你快速构建全面应用
    《ChatGPT全栈开发实战:从需求分析到数据可视化,一站式指南助你快速构建全面应用》是一本旨在引领我们进入全栈开发世界的综合指南。通过结合强大的ChatGPT技术和全栈开发的实践,我们将探索需求分析、后端开发、前端开发、测试、运维、算法优化、数据库、网络安全、人工智能、自然......
  • Deep3DFaceRecon 2D图像转3D模型实战
    本案例通过Deep3DFaceRecon_pytorch实现前置文档:https://github.com/sicxu/Deep3DFaceRecon_pytorchhttps://blog.csdn.net/flyfish1986/article/details/121861086本文是在本地没有gpu硬件的支持下的实现方案,并不具体描述部署过程,部署过程建议看上面两个文档地址准备工程文......
  • 实战:Linux系统存储扩展【已存在挂载点】
    概述此指南提供了在Linux系统中扩展已存在挂载点的步骤,适用于使用LVM管理的逻辑卷。步骤1.识别新硬盘命令:lsblk目的:确定新添加的硬盘的设备名称(例如/dev/vdb)。2.确定卷组名称命令:vgs目的:查找要扩展的逻辑卷所属的卷组名称(如centos)。3.检查文件系统......
  • 7000字详解Spring Boot项目集成RabbitMQ实战以及坑点分析
    本文给大家介绍一下在SpringBoot项目中如何集成消息队列RabbitMQ,包含对RibbitMQ的架构介绍、应用场景、坑点解析以及代码实战。最后文末有免费领取龙年红包封面以及腾讯云社区答题领奖福利,欢迎大家领取。我将使用waynboot-mall项目作为代码讲解,项目地址:https://github.co......
  • 前端必学-40个精选案例实战-案例3-仿QQ邮箱登陆实战
    案例分析:QQ邮箱登录框分析与思路完成这样一个登陆的实现input标签input标签是网页中最常见的输入文字的标签input有很多种类型:例如:密码、文字、数字、颜色、复选等<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Comp......
  • 前端必学-40个精选案例实战-案例4-仿B站导航条案例【基础样式】
    交互方式:CSS样式和HTML的交互方式CSS是控制html的,我们需要选中元素再进行控制CSS的常见使用方式分别是行间式、内嵌式与外链式<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE......
  • 前端必学-40个精选案例实战-案例2-前端必备技能-PS网页切图
    5大图片类型,在网页中的展示方式图片种类图片一般情况下分为两类,一类为像素图,一类为矢量图像素图是由一颗颗像素点组成,如果放大看,会看到像素点矢量图一般是由软件生成,如果放大看,不会产生像素点PS新建保存新建快捷键为Ctrl加n,或者使用菜单功能另存为ctrl(command)加s,或者使......
  • 前端必学-40个精选案例实战-案例1-仿百度图文列表实战
    最终呈现效果理解img标签的作用与效果img标签:前端网页的图片展示标签,单标签,英文非缩写为image常用属性为地址、宽度与高度常见的后缀格式为jpg、jpeg、png、gif等<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Com......
  • 构建知识图谱:从技术到实战的完整指南
    本文深入探讨了知识图谱的构建全流程,涵盖了基础理论、数据获取与预处理、知识表示方法、知识图谱构建技术等关键环节。关注TechLead,分享AI全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师......