在这篇文章中,我们将讨论基于JVM的应用程序内存问题的症状,metaspace引起的内存泄露和溢出问题,以及如何解决这些问题。
症状
以下是一些memory问题的症状:
- 应用程序性能差
- 内存使用异常
- 内存错误(OOME)
应用程序性能差
- 应用程序未达到预期水平
- 响应时间长
- 删除客户端请求
- Stuck threads 卡住
- 服务不可用
- 应用程序日志中的时间戳存在较大间隙
内存问题的原因:
1. 错误配置的内存
- 旧一代内存空间的大小比活动对象集小。这将触发一个主要的垃圾回收(GC),导致更大的暂停。
- 代码缓存小于生成的编译代码占用空间
- 年轻一代的规模不合适,导致对象过早提升
- Metagen/PermGC大小不正确
2. 内存泄漏-对象在内存空间中的意外保留
- 无意中引用堆中的一组对象
- 不适当地取消对类加载器实例的引用
- 未适当释放本机资源
3. 过度使用finalizers终结器
- 具有终结器的对象可能会延迟其自己的GC
- 终结器线程需要在回收实例之前调用实例的
finalize()
方法 - 只能有1个终结器线程。若JVM不能跟上对象可用于终结的速度,那个么JVM会因OOME而失败
- 挂起的终结器对象本质上是累积的垃圾
- Java9中不推荐使用终结器
4. 显式GC调用
System.gc()
和诊断数据收集会导致长时间的暂停-XX:+DisableExplicitGC
可以禁用垃圾回收System.gc()
的调用-XX:+PrintClassHistogram
在接收kill-3
信号时还调用显式GC
OutOfMemory错误
层次结构:Throwable
->Error
->VirtualMachineError
->OutOfMemoryError
(未检查的异常)
当JVM在不同内存空间中的空间不足或无法继续进程执行时抛出。一些可能性:
1. 堆空间已满
- JVM已经调用了完整的GC,但无法释放空间
- 堆的大小可能小于应用程序占用空间,或者应用程序不必要地保留堆中的一些对象集
2. 超出GC间接限制
- 太多的地面军事系统占用的空间非常小
- 应用程序线程没有获得任何CPU周期
3. 请求的阵列大小超过VM限制
4. PermGen空间/java8 metaspace元空间/压缩类空间
- 已调用完整GC,但无法释放元空间中的空间,应用程序正在尝试加载更多类
- Metaspace默认为“unlimited”,但可以由MaxMetaspaceSize控制。默认为压缩类保留1 GB的空间
- 请确保
-Xnoclassgc
未被使用,因为它阻止了类的卸载,否则可能会引起metaspace不够用或Metaspace 占比100
5. 本机内存-本机方法的交换空间/堆栈跟踪不足
- 用于Java线程堆栈、加载的jar、zip、本机库、本地资源(如文件)的本机空间;从本机代码分配的mem
- 无法分配更多本机内存或创建新线程或本机内存泄漏
- 在64位计算机上运行32位JVM会对进程大小施加4GB的限制
- Java堆的位置可以限制本机堆的最大大小。可以由选项
-XX:HeapBaseMinAddress=n
控制,以指定本机堆应基于的地址
CodeCache警告
- JVM打印的警告消息说
CodeCache full,compiler has disabled
。 - 代码缓存已满时没有OOME
- 由Sweeper进行紧急清理。这可能会丢弃已编译的代码,JIT可能需要再次执行优化
- 使用
ReservedCodeCacheSize
选项确保CC大小合适
Direct Buffer Memory直接缓冲存储器
ByteBuffer.allocateDirect(N)
:使用幻象引用和引用队列进行垃圾回收的直接缓冲区- 默认情况下内存不受限制,但可以由
-XX:MaxDirectMemorySize=n
控制 - 由java nio使用。I/O的堆
ByteBuffer
使用临时的direct ByteBuffer
诊断数据、数据收集和分析工具
内存泄漏故障排除
- 确认内存泄漏
- 监视堆使用情况随时间推移
- 如果完整的GCs无法在OldGen中声明空间,则可能是配置问题
- 堆大小可能太小->增加堆大小和监视器!如果问题仍然存在,可能是内存泄漏
-XX:+GCTimeLimit
设置GCs可花费的时间上限,占总时间的百分比,默认值为98%-XX:+GCHEAPFILLIMIT
设置GC应释放的空间量的下限,表示为最大堆的%2%,默认为2%- 如果前5个连续GCs无法将GC成本保持在
GCTimeLimit
以下或至少释放GCHEAPFfRequimit
空间,则在完整GC之后抛出OutOfMemoryError - 如果频繁的完整GCs不要求任何空间,则PermGen/Metaspace可能太小
诊断数据和分析
1. GC日志有助于确定堆需求、找出过多的GC和较长的GC暂停以及内存空间的配置
- 对于Java 9+,G1选项包括:
-Xlog:gc*,gc+phases=debug:file=gc.log . For non G1, -Xlog:gc*:file=gc.log
. 对于较旧的JVM,-XX:+PrintGCDetails,-XX:+printgtimestamps,-XX:+printgdatestamps-Xloggc:gc.log
- 为了检查元空间
-verbose:class or -XX:+TraceClassLoading , -XX:+TraceClassUnloading
- 我们可以通过手工检查,GCViewer,GCHisto来分析日志,gceasy.io
2. 堆转储有助于确定意外的内存增长和内存泄漏。
我们可以通过以下方式进行堆转储:
- jcmd pid GC.heap_dump heapdump.dmp
- jmap -dump:format=b,file=snapshot.jmap pid
- 使用MBean热点诊断的JConsole或Java任务控制
- OOM错误时的JVM选项堆转储:
-XX:+HeapDumpOnOutOfMemoryError
。频繁的完整GCs会延迟堆转储的收集和进程的重新启动
Eclipse内存分析器工具(MAT)显示泄漏嫌疑、直方图、无法访问的对象、重复类、GC根的引用链,允许使用OQL来探索堆转储。
对于JMC和java visualvm,YourKit(一个商业的分析器)都可以接受堆转储。
3. 堆直方图-快速查看堆中的对象
收集使用:
-XX:+PrintClassHistogram
and SIGQUIT on Posix and SIGBREAK 在windows上- jcmd pid GC.class_histogram filename=histo
- jmap -histo pid core_file
- jhsdb jmap (Java 9)
4. Java飞行记录-意外的内存增长和内存泄漏,GC事件
- 启用堆统计信息。会带来额外的性能开销
- 创建飞行记录:
-XX:+UnlockCommercialFeatures-XX:+FlightRecorder-XX:StartFlightRecording=delay=20s,duration=60s,name=Rec,filename=lol.jfr,settings=profile
- 你需要找出泄漏的对象类型,但你需要找出什么是泄漏的对象
5. 终结器
- 使用JConsole、jmap收集数据
- 使用eclipsemat/visualvm使用堆转储进行分析
6. 本机内存
- 本机内存跟踪器输出—跟踪JVM内部使用的本机内存,而不是外部库。使用NativeMemoryTracking选项启动JVM
- pmap,libumem,valgrind,核心文件
下面我们再看一个关于Jenkins的metaspace内存溢出的具体案例:
OutOfMemoryError: Metaspace
从Jenkins 2.60.3升级到Jenkins 2.89.3 LTS后不久,我们体验到java.lang.OutOfMemoryError:元空间错误。我不确定这是否是一个bug,从现象看是metaspace一直百分百,不确认在新版本的Jenkins和pipeline插件中会增加Java元空间。
背景及细节
1. Jenkins v2.60.3
在Jenkins 2.60.3中,我们使用了下面的内存参数,Java非堆空间(Metaspace)保持在350mb和550mb之间。我们跑了好几个月没有重启Jenkins。
-XX:+UseG1GC
-XX:+ExplicitGCInvokesConcurrent
-XX:+ParallelRefProcEnabled
-XX:+UseStringDeduplication
-XX:MaxMetaspaceSize=1g
-XX:MetaspaceSize=256M
-Xms4g
-Xmx8g
-Dgroovy.use.classvalue=true
2. Jenkins v2.89.3
在升级到Jenkins 2.89.3并升级所有插件之后,我们使用了下面的Java内存设置。我们删除了“groovy.use.classvalue=true
“ 我们看到Java非堆内存在250mb和750mb之间波动,并呈上升趋势。几周后,非堆内存达到1GB,Jenkins不再响应请求。日志文件已满java.lang.OutOfMemoryError:Metaspace errors。
-XX:+UseG1GC
-XX:+ExplicitGCInvokesConcurrent
-XX:+ParallelRefProcEnabled
-XX:+UseStringDeduplication
-XX:MaxMetaspaceSize=1g
-XX:MetaspaceSize=256M
-Xms4g
-Xmx8g
调试
我们的Jenkins主机执行大量的Jenkins管道作业,我注意到我们所有的Jenkins 2.89.4主机都显示出比Jenkins升级之前更高的Java非堆内存(java8 metaspace)使用率。我们禁用了管道的耐久性,但仍然可以看到高内存使用率。标准的Java堆内存使用情况看起来不错。
示例1:
Jenkins Master 一号完全闲置,我执行了System.gc()
垃圾回收强制进行完全metaspace gc垃圾回收。后来我发现非堆内存仍然超过700mb。下面是我在完整的GC之后收集的一些Java细节。
VM.native_memory summary
本机内存跟踪:
Total: reserved=11014208KB, committed=5551020KB
Java Heap (reserved=8388608KB, committed=4194304KB)
(mmap: reserved=8388608KB, committed=4194304KB)
Class (reserved=1686933KB, committed=707989KB)
(classes #48326)
(malloc=13717KB #208203)
(mmap: reserved=1673216KB, committed=694272KB)
Thread (reserved=93951KB, committed=93951KB)
(thread #92)
(stack: reserved=93548KB, committed=93548KB)
(malloc=296KB #463)
(arena=107KB #182)
Code (reserved=273528KB, committed=155620KB)
(malloc=23928KB #24978)
(mmap: reserved=249600KB, committed=131692KB)
GC (reserved=412351KB, committed=256703KB)
(malloc=68287KB #693569)
(mmap: reserved=344064KB, committed=188416KB)
Compiler (reserved=406KB, committed=406KB)
(malloc=276KB #2009)
(arena=131KB #3)
Internal (reserved=88791KB, committed=88791KB)
(malloc=88759KB #184270)
(mmap: reserved=32KB, committed=32KB)
Symbol (reserved=30516KB, committed=30516KB)
(malloc=27279KB #301740)
(arena=3236KB #1)
Native Memory Tracking (reserved=22549KB, committed=22549KB)
(malloc=348KB #5361)
(tracking overhead=22201KB)
Arena Chunk (reserved=190KB, committed=190KB)
(malloc=190KB)
Unknown (reserved=16384KB, committed=0KB)
(mmap: reserved=16384KB, committed=0KB)
GC.class_histogram
num #instances #bytes class name
1: 490462 84832616 [C
2: 2552835 40845360 com.cloudbees.groovy.cps.impl.ConstantBlock
3: 930699 37227960 com.cloudbees.groovy.cps.impl.FunctionCallBlock
4: 1493734 35849616 com.cloudbees.groovy.cps.impl.SourceLocation
5: 883507 33258176 [Ljava.lang.Object;
6: 179552 30097544 [B
7: 922229 29511328 java.util.HashMap$Node
8: 1151159 27386104 [Lcom.cloudbees.groovy.cps.Block;
9: 947492 22739808 java.lang.String
10: 790957 18982968 com.cloudbees.groovy.cps.impl.LocalVariableBlock
11: 213822 13097984 [Ljava.util.HashMap$Node;
12: 519301 12463224 com.cloudbees.groovy.cps.impl.SequenceBlock
13: 452808 10867392 java.util.ArrayList
14: 320616 10259712 com.cloudbees.groovy.cps.impl.PropertyAccessBlock
15: 250810 10032400 com.google.common.cache.LocalCache$WeakEntry
16: 168578 9440368 org.codehaus.groovy.runtime.metaclass.MetaMethodIndex$Entry
17: 260734 8343488 java.util.concurrent.locks.ReentrantLock$NonfairSync
18: 250147 7394208 [Lhudson.model.Action;
19: 142590 6844320 java.util.HashMap
20: 139363 6689424 org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode
21: 264178 6340272 com.google.common.collect.SingletonImmutableList
22: 195095 6243040 com.cloudbees.groovy.cps.impl.AssignmentBlock
23: 49477 6237064 java.lang.Class
24: 253041 6072984 java.util.concurrent.CopyOnWriteArrayList
25: 250681 6016344 org.jenkinsci.plugins.workflow.support.storage.BulkFlowNodeStorage$Tag
GC.class_stats
我们使用Pipeline Shared Groovy Libraries插件,注意到有大量的类文件的名称与下面类似。我们的每个共享库类(org.xxxxxxxx.scripts.xxxxxxx)有大约400个引用。
示例2:
我运行了一个堆dump并分析,这是一个测试程序。
Class Name | Objects | Shallow Heap | Retained Heap |
org.jenkinsci.plugins.workflow.support.actions.LogActionImpl | 976,720 | 23,441,280 | >= 343,262,256 |
org.jenkinsci.plugins.workflow.cps.actions.ArgumentsActionImpl | 735,961 | 17,663,064 | >= 305,348,632 |
示例3:
我通过分析器运行了一个不同的Java堆转储,下面是结果。这又是在一个完整的GC之后。
Classes = 35k
Objects = 47M
Class Load = 4.6k
GC Roots = 4.3k
952,652 instances of "org.jenkinsci.plugins.workflow.support.actions.LogActionImpl", loaded by "hudson.ClassicPluginStrategy$AntClassLoader2 @ 0x5c1746598" occupy 342,684,616 (20.83%) bytes. These instances are referenced from one instance of "org.codehaus.groovy.util.AbstractConcurrentMapBase$Segment[]", loaded by "org.eclipse.jetty.webapp.WebAppClassLoader @ 0x5c0000000"
717,629 instances of "org.jenkinsci.plugins.workflow.cps.actions.ArgumentsActionImpl", loaded by "hudson.ClassicPluginStrategy$AntClassLoader2 @ 0x5c17d9770" occupy 296,727,528 (18.04%) bytes. These instances are referenced from one instance of "org.codehaus.groovy.util.AbstractConcurrentMapBase$Segment[]", loaded by "org.eclipse.jetty.webapp.WebAppClassLoader @ 0x5c0000000"
293 instances of "org.jenkinsci.plugins.workflow.cps.CpsFlowExecution", loaded by "hudson.ClassicPluginStrategy$AntClassLoader2 @ 0x5c17d9770" occupy 210,909,800 (12.82%) bytes. These instances are referenced from one instance of "org.codehaus.groovy.util.AbstractConcurrentMapBase$Segment[]", loaded by "org.eclipse.jetty.webapp.WebAppClassLoader @ 0x5c0000000"
Package | Retained Heap | Retained Heap, % | Top Dominators |
---|---|---|---|
workflow | 1,340,511,168 | 81.50% | 5,043,559 |
变通办法
我们再次补充道-Dgroovy.use.classvalue=true
,并注意到Java非堆内存使用的减少。使用该参数,我们可以看到内存增加到750mb左右,但是当服务器空闲时,Java非堆内存将减少到300mb以下。没有这个参数,我们就看不到同样的减少。目前我们的一个Jenkins主机没有参数开始于3/2/18的0.2GB,目前(3/16/18)是0.76GB。曲线图看起来像是缓慢上升,没有太大波动。根据Cloudbees的文章,这个参数不推荐用于Jenkins 2.89,但是它似乎有助于降低Java非堆内存的使用率。由于它每周增加超过200 mb,我应该很快知道是否有某种类型的垃圾回收在第二个接近1GB最大值的Jenkins主机上减少了它。当我手动执行System.gc()
垃圾回收。
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/878.html
暂无评论