4年前 (2020-11-24)  jvm |   抢沙发  872 
文章评分 0 次,平均分 0.0

在这篇文章中,我们将讨论基于JVM的应用程序内存问题的症状,metaspace引起的内存泄露和溢出问题,以及如何解决这些问题。

症状

以下是一些memory问题的症状:

  1. 应用程序性能差
  2. 内存使用异常
  3. 内存错误(OOME)

应用程序性能差

  1. 应用程序未达到预期水平
  2. 响应时间长
  3. 删除客户端请求
  4. Stuck threads 卡住
  5. 服务不可用
  6. 应用程序日志中的时间戳存在较大间隙

内存问题的原因:

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,核心文件

下面我们再看一个关于Jenkinsmetaspace内存溢出的具体案例:

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

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册