深刻了解 JVM – 如何排查分区溢出问题
前言
这篇文章会接续上一篇对于分区溢出的案例实战内容再次补充几个 OOM 的案例,本文不再讲述新内容,以案例实战为主,心愿这些案例能帮忙同学们理解到更多 JVM 对于 OOM 溢出的排查套路。
概述
- Jetty 的底层机制是如何造成间接内存溢出的?如何从景象看到代码设计缺点?
- 线程假死应该如何解决?内存使用率过高会有那些起因?这里将通过一个案例讲述常见剖析套路。
- 队列是如何造成 JVM 堆溢出的,一个简略案例介绍队列数据结构设计的重要性。
前文回顾:
深刻了解 JVM – 分区是如何溢出的?
案例实战
Jetty 的如何造成间接内存溢出的?
案发现场:
首先阐明这种场景并不多见,这里也是收集到一个不错的案例,这个我的项目比拟非凡,应用的不是常见的 Tomcat 服务器而是应用的 Jetty 服务器,在我的项目上线的时候忽然遇到了一个报警,此报警的内容是某一台服务的内容 忽然不能进行拜访了。
此时毫无疑问第一工夫去线上查看日志,服务器挂掉很有可能是 OOM 了,查看一下日志之后发现了如下的内容:
毫无疑问这是一个间接内存的溢出Direct buffer memory。上网搜寻之后咱们得悉,间接内存也叫做堆外内存,不受 JVM 的堆限度,而是由本机的操作系统进行间接治理。接下里咱们来看下为什么会呈现间接内存的溢出。
初步排查:
这里有必要补充一下 Jetty 是个什么玩意:咱们能够简略了解为是和 Tomcat 服务器一样是一个 WEB 容器,他由 Eclipse 基金会组织进行保护,Jetty 也是应用 JAVA 代码写的,所以也能够像 Tomcat 一样间接部署,同时和 Tomcat 一样是一个 JVM 过程,在 Jetty 启动的时候同样会和 Tomcat 一样监听某一个端口,而后你也能够通过发送申请给 Jetty 的模式,实现 MVC 转发等一系列的操作。
既然间接内存会产生溢出,代码下面没有应用到和 NIO 或者 Netty 相干的 API 内容,也没有做间接内存的调配操作,那么通过重复排查最终能够认为是 Jetty 在捣鬼,至于他为什么要应用 Direct Memory 咱们不须要关注,这里波及一个 JVM 的设计缺点,上面咱们会剖析对于间接内存了解和操作形式:
对于 jetty 的间接内存溢出感兴趣的能够看下这一篇文章:Jetty/Howto/Prevent Memory Leaks,上面截取了一段相干内容机翻过来了:
间接字节缓冲区
另一个与 jvm 谬误相干的问题是本机内存耗尽。须要留神的症状是过程大小一直增长,但堆使用量放弃绝对稳固。本机内存能够被很多货色耗费,JIT 编译器是其中之一,而 nio ByteBuffers 是另一个。Sun 谬误编号 **#6210541** 探讨了一个仍未解决的问题,即 jvm 自身在 ** 某些状况下调配了一个从不进行垃圾回收的间接 ByteBuffer**,从而无效地耗费了本机内存。Guy Korland 的博客在这里和这里探讨了这个问题。因为 JIT 编译器是本机内存的一个消费者,因而可用内存的不足可能会在 JIT 中体现为 OutOfMemory 异样,例如“线程“CompilerThread0”中的异样 java.lang.OutOfMemoryError: requests xxx bytes for ChunkPool::allocate. Out 替换空间?”
默认状况下,如果配置了 nio SelectChannelConnector,Jetty 将为 io 调配和治理本人的间接 ByteBuffers 池。它还通过 DefaultServlet 设置将 MappedByteBuffers 调配给内存映射动态文件。然而,如果您禁用了这些选项中的任何一个,您可能容易受到此 jvm ByteBuffer 调配问题的影响。例如,如果您应用的是 Windows,您可能曾经禁用了 DefaultServlet 上动态文件缓存的内存映射缓冲区的应用,以防止文件锁定问题。
首先如果咱们想在绕过 JAVA 的堆在堆外应用一块本机操作的零碎的内存,就须要应用一个叫做 DirectByteBuffer的类,咱们能够应用这个类来实现间接内存的构建,尽管这个对象在构建的时候在堆外面,然而实际上这个对象一旦构建同时会在堆外的内存(操作系统)中构建一个同样的对象跟他进行关联,这里这两块内存能够设想为一个 二级指针 的关系,用形象的了解就是咱们领有一张藏宝图,咱们在藏宝图中标记宝藏,然而实际上的对象在宝藏当中,然而咱们能够通过藏宝图就能够操作宝藏外面寄存的内容,是不是很有意思。
那么你可能会想,这一块内容如何开释呢?其实也很好了解,当你不再援用 DirectByteBuffer 这个对象的时候,这个对象天然会变成垃圾对象,而当他变成垃圾对象被回收的时候天然也把对应的“宝藏”也一并的进行回收。
问题就呈现在这里,当你援用越来越多的间接内存映射的堆对象,然而这些对象又始终没有进行开释,长此以往,间接内存也会不够用,这时候又不能进行回收,那么只能抛出 OOM 的异样了!
个别状况下咱们能够认为 JVM 在霎时调配过多间接内存的状况下可能会导致间接内存溢出,然而这个零碎是这种状况么?从下面的案发现场能够看到,显著不是这么一回事,那么这个案例又是这么个状况呢?
案例剖析:
咱们来持续剖析案例,尽管不是霎时调配大量对象引发的谬误,然而问题起因必定还是间接内存一直的积压导致的,既然这个间接内存对应了堆中的一个映射内容,那么必定也脱不开堆内存这一块,那么会不会是这些对象 压根没有回收过 。很有可能,顺着这个思路,能够发现蕴含了这些间接内存的映射的对象在新生代回收之后 竟然没有被回收掉 ,并且因为 Survior 区域 刚好放不下 而间接进入到了老年代,然而尽管这些对象进入了老年代,然而因为进入的对象很少,也不会触发老年代回收!非常坑爹,所以这里从后果来看,竟然又是对象间接进入老年代引发的问题!
从后果来看,正是堆中映射堆外内存的对象长时间没有被回收,构建的对象同样在 YGC 之后进入老年代,这样的屡次回收之后援用对象越来越多,然而因为老年代没有占用满也不会触发 Full Gc,最终导致了堆外内存的溢出。
NIO 没有思考过回收间接内存?
当然思考过,在 java.nio.Bits
源码包上面的 reserveMemory
有这么一段代码,外面竟然有一个 System.gc()
,这种做法尽管的确有可能触发 Old GC,然而这里切实是要吐槽一句这种做法是谬误并且非常不负责任的!在之前的案例中咱们有一个案例介绍过因为System.gc()
的频繁调用,导致的 FULL GC 过于频繁的问题,所以这里一旦在 JVM 参数中禁止 Sytem.gc()
, 这段代码就齐全生效了。
这里不得不狐疑写这一段代码的人在偷懒,心愿后续的 JDK 在 NIO 这一块能够修复这个问题。
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size, int cap) {
// ....
System.gc();
// ....
}
解决办法:
- 隔靴搔痒,既然是新生代当中的 Survior 区域放不下将要成为垃圾然而临时存活的对象,那么天然须要扩充新生代的堆大小以及 Survior 区域的大小,保障在下一次 YGC 之前把这些间接内存映射的堆中对象给回收掉,确保间接内存不会一直累积。
- 尽管能够放开对于禁止 System.gc() 的参数限度,然而为了你的零碎着想,还是应用第一种办法比拟稳当一些,System.gc()是一个臭名远扬的办法,至于他没有被标记未过期是因为还有很多的代码在应用这个办法,然而咱们开发的时候应该 完全避免应用 这个办法。
第二个案例:线程假死应该如何解决
案发现场:
这个案例是一个十分一般的 Tomcat 以及 WEB 零碎,然而在在某一天忽然报告说服务会呈现 假死 的状况,相当于服务此时是不可应用的,有点相似下面的上游服务宕机的问题。然而这个案例又不一样,过了一会儿又能够拜访了,也就是意味着这种假死只是在一段时间内。
初步排查:
遇到这种问题须要用上一些 Linux 的技巧,留神这里看日志是没有什么成果的,因为并不是 OOM 的问题,所以这里应用 TOP 命令看一下过程对于内存和 CPU 的使用量很有必要。
如果呈现接口上述的假死问题,个别能够依照上面这两种思路进行排查:
- 这个服务可能要应用大量的内存,内存却无奈开释,导致频繁的 GC
- CPU 的负载太高了,过程间接把内存的资源用满了,也就是咱们日常应用零碎过程中的 CPU 负荷过高页面卡死问题,最终导致你的服务线程无奈失去 CPU 的 执行权,而后过程也会被 CPU 给干掉,也就无奈响应接口的申请了。
通过了下面的排查之后,这时候就发现 CPU 消耗很少很少,然而对于内存使用率却达到了 50%!这必定是存在异样的,这里补充阐明一下机器这是一台 4G8 核的机器,JVM 的能够应用 4 -6G 左右的内存,堆的空间大略 3 -4G 左右,所以这时候如果 JVM 过程应用了 50% 的内存,意味着 JVM 不仅把零碎分给它的内存用了一大半。零碎本人的内存也不够用了!
内存使用率高会产生什么?
这里咱们来盘点一下如果零碎的内存使用率过高 JVM 会产生什么状况?
- 内存使用率居高不下,频繁的 Full Gc,Gc 带来的 Stop World 的问题
- 内存使用率过多,JVM 产生 OOM
- 内存使用率过高,过程可能因为申请内存不足,导致操作系统间接杀过程。
那么下面的排查和这几个点那个关系比拟大呢?
案例剖析
依据下面的内容,咱们来一一剖析一下这些状况:
首先,第二点产生 OOM 的状况咱们能够间接排除,因为这个案例的状况是假死,然而日志排查 并没有 OOM的状况,所以能够不再思考。接着咱们来看下第一点和第三点。
首先是第一点,是对于频繁 Full GC 和 Stop World 的,然而理论排查的过程中 GC 的耗时每次也就 几百毫秒,所以尽管 GC 的频率高,然而没有占用十分多的工夫,属于失常的状况。
最初,咱们重点来看下第三点,第三点说的是申请内存不足导致杀过程的问题,这里须要揭示一点的是 整个零碎会有自动化检查和启动脚本,当服务宕机的时候会主动启动,很贴近第三点的问题,就是杀完过程之后,零碎又主动启动之后又主动失常了。
谁占用了许多内存
这里独自再开一个大节阐明一下为什么会占用这么多内存,排查过程这里不再细说,总之就是通过 MAT 等工具剖析得悉有一个对象长期占用并且没有方法进行回收,那就是 自定义类加载器,工程师本人开发了一个类加载器,然而因为没有管制好逻辑导致线程创立了大量的类加载器一直的加载,最初导致这些大量对象积压在老年代然而 Full Gc 发现存在援用又不能回收掉。
解决办法:
此案例问题是频繁创立类加载器导致 Full Gc 频繁又没法回收,所以最终的解决办法就是批改代码保障不要反复创立类加载器即可。
第三个案例:同步零碎的溢出问题
案发现场:
这个案例是一个同步零碎,负责从一个零碎向另一个零碎进行数据的同步操作,两头应用 kafka 中间件进行生产和生产的工作。
问题是什么呢?这个问题是一个 OOM 的问题,在 OOM 之后须要手动进行重启,后果重启之后又呈现了 OOM,最终 Kafka 的数据量越来越大,GC 的频率也越来越高,最终导致系统宕机 OOM。
初步排查:
JVM 堆内存溢出无非就是两种状况,要么是堆内存进行 Full GC 之后仍然有很多的存活对象,要么是一瞬间忽然产生一大堆的对象在堆中无奈寄存导致 OOM,这两者状况都是有可能的。
这里再剖析下,这个案例是一段时间之后才溢出的,同时即便要解决的数据变多也并不是一旦启动就立马溢出,而是 OOM 的频率变高了而已。这么看来,能够确认是因为存活对象过多赖在内存外面导致解决数据加载到堆之后老年代又放不下立马就 FULL GC 了,而后 FULL GC 之后发现还是很多对象,最终老年代累积到 100%,后果就只能 OOM 了。
案例剖析:
咱们应用 Jstat 进行案例的剖析,果然发现 Full Gc 之后对象没有进行回收,当老年代空间为 100% 之后,对象无奈实现调配最终只能溢出并且进行 JVM 过程了。
这里仍然应用了 MAT 的工具进行剖析,这个剖析在专栏之前的案例进行过图解剖析,这里就不在啰嗦了。
接下来咱们来剖析下忽然泄露的根本原因,这时候须要留神下 Kafka 这个音讯队列了,Kafka 是什么货色这里不再多说,它在案例中次要的工作是一直推入两边的数据进行数据的生产生产的同步操作。
重点来了,咱们都晓得生产数据能够一次性生产几百条的,因而这时候就有开发人员为了不便生产,把生产的内容设置为了List,并且每一个 List 有几百条数据。这种设计构造在生产的时候有什么状况呢?这里做一个比喻就是传送带上的产品,原本每一道流程的负责人只须要加工传送带上的一份数据,然而应用这个后果就好比在每一份传送带的产品上挂着 100 多个产品要进行解决!这样生产方毫无疑问是生产不过去的,而生产方又在一直的推送数据,最终只能罢工(OOM)了。
解决办法:
这是一个典型的生成和生产速率不对等的案例,这里的解决办法就是把队列设置为阻塞队列,比方设置 1024 个大小,一旦队列满了,生产方就会进行生成并且阻塞监听这个队列,当队列一旦有空间再开启生产工作,这样生产就能够及时处理,也就不会造成对象积压在堆内存而没方法回收了。
总结
这一次又是三个案例,这里用三个不同状况的案例,用系统分析的角度去剖析他们的细节局部,这里还是再提一点,线上 OOM 的状况是千奇百怪的,心愿学习这些案例更多的是学习解决思路而不是去背案例,因为现实情况下既有可能是资源分配问题,又有可能是零碎代码问题,深圳有可能是配置参数有问题导致系统宕机,总之问题是千奇百怪的,到目前为止也没用很万金油的解决方案,然而通过学习这些案例,咱们有了肯定的实践根底,未来遇到问题的时候,能够很快的回忆案例并且进行避坑。
写在最初
到此对于案例实战的局部就曾经齐全完结了,从专栏的下一篇开始,将会略微深刻一些 JVM 的底层,当然这些常识还是从书上总结来的,如果感觉了解有难度倡议浏览一下周大神的《深刻了解 JVM 虚拟机第三版》。
最初举荐一下集体的微信公众号:“懒时小窝”。有什么问题能够通过公众号私信和我交换,当然评论的问题看到的也会第一工夫解答。
历史文章回顾:
留神这里应用的是“有道云笔记”的链接,不便大家珍藏和自我总结:
深刻了解 JVM – 阶段总结与回顾(二)
深刻了解 JVM – 案例实战