OOM简记

作者: cqxxxxxxxx | 来源:发表于2021-05-26 17:25 被阅读0次

    OOM简记

    notice: 下面说的比如10M老年代空间,在10M分配完毕的时候进行FullGC都是简化的说法,其实应该是有个空间分配担保机制的存在,不会出现在10M全部使用的情况下才进行FullGC的情况。

    1. OOME出现的区域

    1. heap java堆

    2. vm stack 虚拟机栈

    3. native method stack 本地方法栈

    4. method area 方法区

    5. direct memory 直接内存

    ps. 除了program counter register 程序计数器外的其他内存区域都有可能发生oome

    2. 怎么判断OOME出现的区域

    查看异常堆栈信息,一般在OOME后会进一步跟着提示具体的区域,比如

    Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
     at com.cqx.oom.OOMDemo.lambda$main$0(OOMDemo.java:23)
     at com.cqx.oom.OOMDemo$Lambda$1/668386784.run(Unknown Source)
     at java.lang.Thread.run(Thread.java:748)
    

    3. 什么时候会抛出OOME

    当创建对象时jvm检测到内存不够本次内存分配,于是会进行一次FullGC,如果本次GC后还是内存不够分配,那就抛出OOME,当前的线程就会死亡。

    OOME Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. ----API文档

    The JVM will run the GC when it's on edge of the OutOfMemoryError. If the GC didn't help at all, then the JVM will throw OOME.

    具体情况可能发生OOME的情况如下

    假设jvm的参数设置现在是 -Xmx20M -Xmx20M -Xmn10M -XX:MaxDirectMemorySize=1M, 即分配堆大小为20M,其中老年代和新生代各10M
    public static final int _1MB = 1024 * 1024;
    1. 持续不断创建对象,并不释放他的引用
    for(i = 0; i < 100; i++) {
    byte[] M1 = new byte[_1MB];
    }
    随着对象不断创建,tenured generation到达了10MB,即已经装满了,然后继续创建对象,随着新生代中Eden区再次被填满,触发一次Minor GC,之前Survivor0中部分对象由于各种原因,比如年龄大了,或者本次MinorGC中发现S0不够存放本次存活的对象,所以会有对象晋升到Old Generation。此时JVM发现老年代的内存也不够进行此次分配,于是就进行一次FullGC(Major GC),根据可达性分析,从GC ROOTS开始分析对象的引用关系,发现任然存活,就不进行回收,所有本次FullGC并没有任何卵用,所有GC后还是分配不了,那么此时当前线程就会抛出一个OOME,然后死亡。

    2. byte[] _12MB = new byte[12 * _1MB];
    大对象的创建会根据JVM参数的设置直接分配到老年代中,跟1类似,FULLGC后发现内存不够,抛出OOME

    3. ByteBuffer.allocateDirect(2 * _1MB);
    由于之前指定了最大直接内存为1MB 这边分配了2MB就抛出了OOME
    java.lang.OutOfMemoryError: Direct buffer memory

    4. 方法区OOME
    之前在第一次接 Jenkins发布平台时就出现了这个问题。当时发布www?指定的jdk版本是7,可能是因为www大量的jsp文件(感觉也不多),在编译生成class文件时导致方法区不够用发生了OOME。看当时打印的异常日志确认是方法区的OOME。
    原因: jdk8之前版本不通过-XX:PermSize和-XX:MaxPermSize显示指定方法区大小,应该就是64MB。当时的jenkins中配置只指定了堆大小,没有指定方法区的大小
    解决: 1. -XX:PermSize和-XX:MaxPermSize 2.改用jdk8(移除了使用永久代来当做方法区的策略,使用了metaspace元数据区,不暂用jvm指定的堆内存,而是使用机器的native memory,不受jvm堆的限制)

    5. 调用外部服务,外部服务故障或者处理缓慢导致OOME
    比如调用中交兴路查询位置接口,网络或者ZJXL服务原因请求一直得不到反馈,两边处理速度不对等,导致越来越多的请求积压,OOME。
    思路:
    a. 使用hystrix等熔断功能的工具来管理外部服务调用
    b. 复用tcp连接。可以考虑加上keepalive:true的请求头,避免每次调用都重新发起tcp连接,
    c. httpclient这种应该是要单例的,不需要每次请求都单独创建
    d. 生产者消费者模式来处理

    6. 内存泄漏
    a. 监听器和一些回调注册上来但是没有显示的取消,服务端一直持有这个监听器的引用。
    b. 缓存泄漏,比如用HashMap实现的缓存,吧引用扔到进去后忘了,一直扔一直扔就炸了,GC的时候发现对象引用仍然在缓存里,就不回收就炸了。可以用WeakHashMap来实现,key是WeakReference,GC可以回收掉。
    </pre>

    4. OOME会导致JVM shutdown吗

    不会。OOME只会把抛出这个异常的线程给杀掉。 之所以发生OOME的时候应用程序经常出现假死,是因为OOM了,其他正常运行的线程也无法分配到资源,各种请求都无法得到处理,给人一种应用挂掉的感觉。

    如果你想在OOME的时候主动kill掉当前的应用可以
    -XX:OnOutOfMemoryError="kill -9 %p"  这里还可以执行你的脚本,这样就可以自动重启。。。
    %p is the current Java process PID placeholder.
    

    5. 上面说的OOME会杀死当前的线程,那么GC可以回收到这部分资源,释放空间,那么为什么应用程序还是无法响应请求呢。

    因为往往抛出OOME的那个线程,不是大头,他只是压垮骆驼的最后一根稻草。

    比如一个应用,堆最大设为10M,一个定时任务的线程读取数据,在内存里生成了上万个对象,进行处理,此时用了9.9MB内存空间。然后一个普通的查询请求进来,OOME了,这个请求线程死亡了,由于定时任务线程还在处理,GC只把请求线程的资源回收。那么在定时任务线程处理期间,还是会一直出现OOME。

    6. JVM可以自动从OOME恢复吗

    可以的,但是不建议。

    例如5中的情况,定时任务线程处理完毕了,那么GC会吧这部分内存释放掉,程序又正常了。 但是发生OOME的时候不建议让JVM自动恢复。因为出现了这个异常一定是你程序上有漏洞,有问题,就算本次恢复了,接下去很可能还是会出现这个问题。而且OOM恢复期间很难熬,不管是对于JVM还是用户来说。具体其他的可以看下面的链接

    比如之前遇到的,之前做的司机证件导入功能,就发生过一次OOME。这个功能是处理用户导入的照片文件,服务器接收这些文件,处理并存放到MongoDB。当时用户间隔很短时间导入两次大约1G的照片文件,第一次导入的请求还在执行中,因为照片需要读取到内存,照片文件序列化生成的大对象,直接进入老年代,MinorGC没办法清理,占用了大量的内存。紧接着第二次导入请求进来了,在处理过程中由于内存不够OOME了。但是这并不影响第一次的那个请求线程。当时jmap -dump:live,format=b,file=path pid dump了堆栈信息,发现内存够用啊怎么会发生OOME呢,觉得很奇怪。然后我让用户再次导入,发现还是OOME。后面就突然发现-dump:live或者-histo:live会先触发一次FullGC再进行内存信息收集,所以出现异常的那个线程的内存都被回收掉了,所以dump文件上看来是正常的。 所以我就让用户先别导,过个半个小时在来操作,这样就好了。

    live指令的解释
    dump only live objects; if not specified, all objects in the heap are dumped.</pre>
    

    7. 怎么应对OOME

    1. 考虑在JVM启动参数就设置上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath,但是有风险,比如5中例子,一直会触发OOME,一直会生成dump文件,会更加麻烦,所以可以考虑加上-XX:OnOutOfMemoryError="kill -9 %p"

    2. 如果1没做,那么可以考虑用jmap来生成dump堆栈快照迅速保留现场信息,并重启应用(1 2G可能几分钟就好了????没找到数据,懒得试)。在此之前可以用jstats查看GC的情况

    3. 如果JVM的分配内存很大,好几G,jmap可能需要执行很久才生成dump文件,可以考虑用gdb来处理,好像会快很多。不管他不是jdk自带的工具,要自己下载。参考链接

    4. 考虑使用softReference、weakReference、weakHashMap等等。

    5. 不用jmap,使用编程式方式触发生成当前的堆转储快照,参考链接

    8. 堆转储快照分析

    工具: jhat/visualVM/JPofiler/MAT 语句: OQL(对象查询语句)

    9. spring actuator/JMX Java Management Extensions

    actuator 应用监控的东西,暴露了一些http接口,有个事heapdump还有个stackdump,可以通过定时任务之类去访问然后分析,如果有问题就通知之类的。

    jmx 公司有平台radar。可以多学习下这方面的东西。

    10. 死锁与死循环,CPU飙升相关排查

    1. jstack 看线程堆栈

    2. top查找cpu占用率高的pid top -p pid -H 查看进程内线程的pid

    3. pid转16进制,去jstack中查日志

    https://blog.51cto.com/13732225/2347907

    . . . . . .

    参考的文档链接

    https://stackoverflow.com/questions/3058198/can-the-jvm-recover-from-an-outofmemoryerror-without-a-restart

    https://stackoverflow.com/questions/12096403/java-shutting-down-on-out-of-memory-error

    https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

    https://segmentfault.com/a/1190000010603813

    https://stackoverflow.com/questions/6418089/does-jmap-force-garbage-collection-when-the-live-option-is-used

    相关文章

      网友评论

          本文标题:OOM简记

          本文链接:https://www.haomeiwen.com/subject/nsaksltx.html