当堆内存被塞满之后,一边 GC 无法及时回收,一边又在连续创建新工具,Allocator 无法分配新的内存之后,就会送一个 OOM 的缺点:
java.lang.OutOfMemoryError: Java heap space
剖析办理起来无非是那几步:
dump 堆内存通过 MAT、YourKit、JProfiler 、IDEA Profiler 等一系列工具剖析dump文件找到占用内存最多、最大的工具,看看是哪个小可爱干的剖析代码,考试测验优化代码、减少工具创建增加 JVM 堆内存、限定要求数、线程数、增加节点数量等常见类库利用误区尤其是一些工具库,尽可能的避免每次新建工具,从而节省内存提升性能。
大多数主流的类库,入口类都担保了单例线程安全,全局掩护一份即可
举一些常见的缺点利用例子:
Apache HttpClientCloseableHttpClient ,这玩意相称于一个“浏览器进程”了,背后有连接池连接复用,一堆机制的赞助类,如果每次都 new 一个,不仅速率慢,而且摧残浪费蹂躏了大量资源。
比较正常的做法是,全局掩护一个(或者根据业务场景分组,每组一个)实例,做事启动时创建,做事关闭时销毁:
CloseableHttpClient httpClient = HttpClients.custom() .setMaxConnPerRoute(maxConnPerRoute) .setMaxConnTotal(maxConnTotal) /// ... .build();
Gson
毕竟是 Google 的项目,入口类自然也是实现了线程安全,全局掩护一份 Gson 实例即可
JacksonJackson 作为 Spring MVC 默认的 JSON 处理库,功能强大、用户浩瀚,xml/json/yaml/properties/csv 各种主流格式都支持,单例线程安全自然也是 ok 的,全局掩护一份 ObjectMapper 即可。
GC overhead limit exceeded这个缺点比较故意思,上面的 Java heap space 是内存彻底满了之后,还在持续的创建新工具,此时做事会彻底假去世,无法处理新的要求。
而这个缺点,只是表示 GC 开销过大,Collector 花了大量的韶光回收内存,但开释的堆内存却很小,并不代表做事去世了
此时程序处于一种很奇妙的状态:堆内存满了(或者达到回收阈值),一直的触发 GC 回收,但大多数工具都是可达的无法回收,同时 Mutator 还在低频率的创建新工具。
涌现这个缺点,一样平常都是流量较低的场景,有太多常驻的可达工具无法回收,但是吧,GC 后空闲的内存还可以知足做事的基本利用
不过此时,已经在频繁的老年代GC了,老年代又大工具又多、在现有的回收算法下,GC 效率非常低并切资源占用巨大,乃至会涌现把 CPU 打满的情形。
涌现这个缺点的时候,从监控角度看起来可能是这个样子:
要求量可能并不大一直 GC,并切停息韶光很永劫时时的还有新的要求,但相应韶光很高CPU 利用率很高毕竟还是堆内存的问题,排查思路和上面的 Java heap space 没什么差异。
Metaspace/PermGenMetaspace 区域里,最紧张的便是 Class 的元数据了,ClassLoader 加在的数据,都会存储在这里。
MetaSpace 初始值很小,默认是没有上限的。当利用率超过40%(默认值 MinMetaspaceFreeRatio)会进行扩容,每次扩容一点点,扩容也不会直接 FullGC。
比较推举的做法,是不给初始值,但限定最大值:
-XX:MaxMetaspaceSize=
不过还是适合心,这玩意满了后果很严重,轻则 Full GC,重则 OOM:
java.lang.OutOfMemoryError: Metaspace
排查 MetaSpace 的问题,紧张思路还是追踪 Class Load数据,比较主流的做法是:
通过 Arthas 之类的工具,查看 ClassLoader、loadClassess 的数据,剖析数量较多的 ClassLoader 或者 Class打印每个 class 的加载日志:-XX:+TraceClassLoading -XX:+TraceClassUnloading下面先容几个常见的,可能导致 MetaSpace 增长的场景:
反射利用不当JAVA 里的反射,性能是非常低的,以反射的工具必须得缓存起来。尤其是这个Method工具,如果在并发的场景下,每次都获取新的 Method,然后 invoke 的话,用不了多久 MetaSpace 就给你打爆!
大略的说,并发场景下,Method.invoke 会重复的动态创建 class,从而导致 MetaSpace 区域增长,详细剖析可以参考笨神的文章《从一起GC血案谈到反射事理》。
用反射时,尽可能的用成熟的工具类,Spring的、Apache的都可以。它们都内置了reflection干系工具的缓存,功能又全性能又好,足以办理日常的利用需求。
一些 Agent 的 bug一些 Java Agent,静态的和运行时注入的都算。基于 Instrumentation 这套 API 做了各种增强,一会 load 一会 redefine 一会remove的,如果欠妥心涌现 BUG,也很随意马虎天生大量动态的 class,从而导致 metaspace 打满。
动态代理问题像 Spring 的 AOP ,也是基于动态代理实现的,不管是 CgLib 还是 JDK Proxy,不管是 ASM 还是 ByteBuddy。终极的结果都逃不开动态创建、加载 Class,有这两个操作,那 Metaspace 必定受影响。
Spring 的 Bean 默认是 singleton 的,如果配置为 prototype,那么每次 getBean 就会创建新的代理工具,重新天生动态的 class、重新 define,MetaSpace 自然越来越大。
Code CacheCode Cache 区域,存储的是 JIT 编译后的热点代码缓存(把稳,编译过程中利用的内存不属于 Code cache),也属于 non heap 。
如果 Code cache 满了,你可能会看到这么一条日志:
Server VM warning: CodeCache is full. Compiler has been disabled.
此时 JVM 会禁用 JIT 编译,你的做事也会开始变慢。
Code Cache 的上限默认比较低,一样平常是240MB/128MB,不同平台可能有所差异。
可以通过参数来调度 Code Cache 的上限:
-XX:ReservedCodeCacheSize=
只要只管即便避免过大的Class、Method ,一样平常也不太会涌现这个区域被打满的问题,默认的 240MB/128MB 也足够了
Direct MemoryDirect Memory 区域,一样平常称之为直接内存,很多涉及到 磁盘I/O ,Socket I/O 的场景,为了“Zero Copy”提升性能都会利用 Direct Memory。
就比如 Netty ,它真的是把 Direct Memory 玩出了花(有空写一篇 Netty 内存管理剖析)……
利用 Direct Memory时,相称于直接绕过 JVM 内存管理,调用 malloc() 函数,体验手动管理内存的乐趣~
不过吧,这玩意利用比较危险,一样平常都合营 Unsafe 操作,一个欠妥心肠址读写的地址缺点,就能得到一个 JVM 给你的惊喜:
## A fatal error has been detected by the Java Runtime Environment:## EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffdbd5d19b4, pid=1208, tid=0x0000000000002ee0## JRE version: Java(TM) SE Runtime Environment (8.0_301-b09) (build 1.8.0_301-b09)# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.301-b09 mixed mode windows-amd64 compressed oops) # Problematic frame:# C [msvcr100.dll+0x119b4]# # No core dump will be written. Minidumps are not enabled by default on client versions of Windows## If you would like to submit a bug report, please visit:# http://bugreport.java.com/bugreport/crash.jsp# The crash happened outside the Java Virtual Machine in native code.# See problematic frame for where to report the bug.#
更多的阐明,可以参考我这篇《Java中的Heap Buffer与Direct Buffer》
这个 Direct Memory 区域,默认是无上限的,但为了防止被 OS Kill,还是会限定一下,给个256MB或者更小的值,防止内存无限增长:
-XX:MaxDirectMemorySize=
如果 Direct Memory 达到 MaxDirectMemorySize 并且无法开释时,就会得到一个 OOM缺点:
java.lang.OutOfMemoryError: Direct buffer memory
Linux OOM Killer
跳出 JVM 内存管理之后,当 OS 内存耗尽时,Linux 会选择内存占用最多,优先级最低或者最不主要的进程杀去世。
一样平常在容器里,紧张的进程便是肯定是我们的 JVM ,一旦内存满,第一个杀的便是它,而且还是 kill -TERM (-9)旗子暗记,打你一个惊惶失措。
如果 JVM 内存参数配置合理,远低于容器内存限定,还是涌现了 OOM Killer 的话,那么恭喜你,大概率是有什么 Native 内存泄露。
这部分内存,JVM 它还管不了。
除了 JVM 内部的 Native 泄露 BUG 这种小概率事宜外,大概率是你引用的第三方库导致的。
这类问题排查起来非常麻烦,毕竟在 JVM 之外,只能靠一些原生的工具去剖析。
而且吧,这种动不动就要 root 权限的工具,可是得领导审批申请权限的……排查本钱真的很高
排查 Native 内存的基本的思路是:
pmap 查看内存地址映射,定位可疑内存块、剖析内存块数据strace 手动追踪进程系统调用,剖析内存分配的系统调用链路改换jemalloc/tcmalloc之类的内存分配器(或者 async-profiler有个支持native 剖析的分支)追踪malloc的调用链路目前最常见的 Native 内存泄露场景,是 JDK 的 Inflater/Deflater 这俩卧龙凤雏,功能是供应 GZIP 的压缩、解压,在默认 glibc 的 malloc 实现下,很随意马虎涌现“内存泄露”。如果涌现 Native 内存泄露,可以先看看运用里有没有 GZIP 干系操作,说不定有惊喜。
好了,各种风格的 OOM 都感想熏染完了,到底哪一个更能打动你呢?