JVM内存分配
- NMT是了解JVM内存分配的最佳途径, 但一定不要相信commited的数据(比如Thread, JNI和malloc的缺陷).
- 检查内存占用最准的工具是ps查看rss, 进程物理内存占用.
- 如果在Container/Pod里使用cgroup限制了内存, 加就直接看系统历史最大内存占用. 因为cgroup就是按这个数值来杀pod的. /sys/fs/cgroup/memory/memory.max_usage_in_bytes
- Total memory = Heap + Code Cache + Metaspace + Symbol tables + Other JVM structures + Thread stacks + Direct buffers + Mapped files + Native Libraries + Malloc overhead + ...
- 参考另一篇笔记 :JVM parts (Native Memory Tracking) https://stackoverflow.com/questions/53451103/java-using-much-more-memory-than-heap-size-or-size-correctly-docker-memory-limi
内存分析常用命令
- top, free. 查看内存占用, free = total - used - buff/cache. 查看进程的物理内存RES占用. 有I/O的时候还会占用buff.
shift+m按MEM排序. 如果想按RES排序: shfit+f在RES上按s再按esc
- jps, 获得进程号
- ps -aux 输出的RSS是进程的物理内存占用, 应该和jcmd 1 VM.native_memory summary输出的committed相符. 否则就是JNI Native Memory内存占用. RSS=pmap -x 7, 使用pmap可以查看具体内存占用, anon类型的就是gclib给JNI分配的
- jmap -histo:live 1 无法回收的实例数量和内存占用, live参数会触发GC
- jmap -heap 7 获堆空间分配报告, 参考<<jmap heap 堆参数分析>>
- jmap -dump:format=b,file=heap.dump 1 导出堆内存
- Native Memory Tracking 检查内存变化, 包括堆外内存, from Java8
- jcmd 1 VM.native_memory summary 检查堆外内存占用. HotSpot专用, 需要提前用JVM参数启用-XX:NativeMemoryTracking=summary. Reserved只是申请了页地址, Committed才是实际占用的内存.
- jcmd 1 VM.native_memory baseline 记录NMT基准点
- jcmd 1 VM.native_memory summary.diff 和baseline对比内存变化
- jcmd 7 VM.native_memory detail.diff 和baseline对比内存变化
- pmap导出的进程内存不会显示JVM的使用细节, 只有对应NMT报告的地址才能解释
- pmap -x 1 | sort -n -k3 > pmap.txt 查看内存分配情况, 缺页次数, [ anon ] 为mmap等堆外内存占用. 这里的地址段和NMT报告的地址是对应的. Reserved(PROT_NONE) -> commited(JVM已经管理和使用的内存, 计算在rss里面) -> segfault(commited内存不足时触发) -> increase size(从reserved部分commite更多的内存).
可以使用-XX:+AlwaysPreTouch强制JVM直接commit所有内存, 防止缺页中断影响性能.
如果在pmap里发现大量的64MB匿名块, 可能是Linux内存分配的bug, 这些块(由DirectBuffer申请)不会被回收, 也不会计算在NMT的内存里. 如果rss和nmt的数据相差很大, 可以考虑替换系统malloc为:jemalloc(FreeBSD, 推荐, --enable-prof), tcmalloc(Google)或者mimalloc(MSFT). - 使用gdb导出内存并分析内容, JDK9开始可以使用jhsdb, OpenJDK可以使用clhsdb
- apk add gdb
- gdb --batch --pid 1 --ex "dump memory mem.dump 0x7fc181025000 0x7fc181026000"
- strings -n 10 -f mem.dump 查找内存文件中长度超过10的字符串
- jstat 分析GC数据, gc基本gc情况
gcutil 按百分比显示
gccause显示原因. EC: Eden Capacity, EU: Eden Usage. OC: Old Capacity, OU: Old Usage
jstat官方文档: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html - jcmd 7 VM.flags 获取JVM运行时隐含参数
jcmd 7 VM.info 获取JVM所有信息
jcmd 7 VM.info 获取JVM所有信息 - jstack 打印线程信息, 放在https://fastthread.io/ 分析
如果线程很多, 可能占用很多栈空间.
而且还可能出现HeapByteBuffer的缓存问题, 当使用 HeapByteBuffer 进行网络通信等 IO 操作时,由于只有“本地”内存才能传递给操作系统调用,此时会将数据复制到一个临时的 DirectByteBuffer 对象中,JDK 会为每个线程缓存这个临时对象,且不限制内存大小。因此,如果程序有多个线程生成很多大 HeapByteBuffer 对象时,且线程一直存活,则会导致进程会占用大量的本地内存,造成内存泄露。
https://blog.csdn.net/u012099869/article/details/82870082
解决办法是用-Djdk.nio.maxCachedBufferSize=262144,来限制这个缓存的大小. Jdk8 u102 - 获取垃圾回收日志
添加JVM启动参数:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -Xloggc:/opt/scratch/$(MY_POD_NAME)_gclog.txt
在另一个vm tail -f 这个文件, 可以解决vm重启覆盖的问题.
把gc log上传到https://gceasy.io/ 可以获得分析报告. 例子在最下面. - 查找Java Home java -XshowSettings:properties -version
- CGroup的内存限制
如果使用-XX:+UseCGroupMemoryLimitForHeap参数动态配置堆大小.
遇到容器报OOMKill错误时, 需检查堆外内存(non-heap)占用
/sys/fs/cgroup/memory/memory.limit_in_bytes是CGroup设置的内存上限, 超过就会kill pod
/sys/fs/cgroup/memory/memory.max_usage_in_bytes是pod历史最大内存占用, 要和上一个保持一定差距
下图只剩79MB是比较危险的, 可以限制一下最大堆外内存.
优化方式: 系统稳定运行一段时间后, 使用下面的参数把上面两个内存的差距调小.
-XX:MaxRAMPercentage, 这是设置Xmx = memory.limit_in_bytes * percentage的
剩下的堆外内存部分有各种限制参数, 不建议全部写死, 参考下面"限制每一个内存区域的大小"的部分 - 命令行访问JMX MBean
- 下载命令行工具Jmxterm https://github.com/jiaqi/jmxterm
- 启动ManagementAgent jcmd 1 ManagementAgent.start_local
- 读取MBean数据 echo "get -d java.lang -b name=Code\ Cache,type=MemoryPool Usage" | java -jar jmxterm-1.0.1-uber.jar -l 1 -n
- 关闭ManagementAgent jcmd 1 ManagementAgent.stop
- 查看已经被杀掉的进程的信息(Pod被删除的不适用)
dmesg|grep -i kill - G1GC优化参数
- -XX:MarkStackSize
- -XX:MaxGCPauseMillis
- -XX:MaxRAMPercentage = <= 90.0 (heap size, default 25)
- -XX:MaxDirectMemorySize = 不建议限制, 最大不能超过heap size - metaspace(默认=heap size).
- -XX:G1HeapRegionSize = 物理内存/2048 (byte)
- -XX:ParallelGCThreads= 逻辑处理器数量
- -XX:ConcGCThreads= <ParallelGCThreads/4 ~ ParallelGCThreads/2>
- 不要配置-XX:+DisableExplicitGC禁用System.gc(), 有的库需要主动调用gc回收DirectBuffer. 可以使用-XX:+ExplicitGCInvokesConcurrent代替
- G1GC参考资料
- G1 解决Evacuation Failure和Humongous Allocation
单独有一篇笔记 https://blog.csdn.net/u011381576/article/details/79755069 - 限制每一个内存区域的大小
- Class : -XX:MaxMetaspaceSize(默认=Xmx大小, 但不使用Xmx堆空间), -XX:CompressedClassSpaceSize(包含在MaxMetaspaceSize之内, 默认1G). 不要使用-XX:MetaspaceSize, 会触发GC
- Code: -XX:InitialCodeCacheSize, -XX:ReservedCodeCacheSize (默认240MB, 如果满了会清空已编译的代码, 导致极大的性能损失. 可以用profiler检查空间占用)
- Compiler(Arena): 使用C1+C2会增加Code的内存占用?
- Thread: 默认一个stack 1mb,实际commit了也不一定占用内存, 这块会导致NMT的commit和实际系统占用不一致. -Xss可以设置每个线程的stack大小.
- Symbol: SymbolTable和StringTable的空间, 基本不会占用太多. 使用jcmd 7 VM.stringtable和jcmd 7 VM.symboltable查看占用. 100万字符串700MB.
- Internal:著名的直接内存访问区, JDK11转入Other区, 使用DirectBuffer.mmap和Unsafe申请, 用-XX:MaxDirectMemorySize限制, 默认也等于Xmx(联合Metaspace已经用了三份Xmx内存了...怪不得Xmx默认25%内存...). 可以用MBean java.nio.BufferPool.direct查看使用情况. DirectBuffer能被gc回收但不会主动触发GC, 满了就会OOM, 有的库会主动调用System.gc()回收DirectBuffer, 所以不要配置-XX:+DisableExplicitGC禁用System.gc(), 可以使用-XX:+ExplicitGCInvokesConcurrent代替.
- Linux perf, 如果出现JVM监控范围之外的内存泄漏, 可以参考另一篇文章使用perf监控系统tracepoint https://developer.aliyun.com/article/65255
内存分析常用工具
- Eclipse Memory Analyzer, 自带内存泄漏检测功能 https://www.eclipse.org/mat/downloads.php
- jconsole 图形化调试工具, 可以看线程, 内存情况, 读取配置MXBean. 还可以远程连接JVM的jmxremote接口, from Java 5 by Sun
- jmc Java Mission Control 图形化调试工具, 可以分析jcmd记录的JFR(收费), from Java 7, (BEA JRockit)
- jvisualvm Visual VM, from Java 6 u7 (NetBeans), recommand use open source version http://visualvm.github.io/
- http://gceasy.io/ 根据gc log生成报告.
参考资料
- NMT: https://juejin.im/post/5c986e48f265da60cc02b83c
- Understanding Java Memory Model: https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973
- Andrei Pangin: Memory footprint of a Java process: https://vimeo.com/364039638
- Andrei Pangin: NMT: https://stackoverflow.com/questions/53451103/java-using-much-more-memory-than-heap-size-or-size-correctly-docker-memory-limi
Comments