java内存泄漏分析

举报
牛牛同学 发表于 2022/05/18 11:49:55 2022/05/18
【摘要】 列举几种常见内存泄露问题及定位解决方法

最近项目中遇到了一些内存泄漏问题,定位过程中绕了很多弯路,总结下经验,如有不正之处欢迎大家指正。

程序出现内存泄漏后,首先需定位出泄漏点,而往往定位问题是最困难的,定位方法多种多样,下面讲下我的思路。


java运行时内存区域

图片来源:https://cloud.tencent.com/developer/article/1408827

1.Java Heap 泄露

        堆溢出一般会抛出java.lang.OutOfMemoryError: Java heap spaceOutOfMemoryError后有Java heap space就是堆溢出JVM在启动的时候会自动设置JVM Heap的值,可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。在JVM中如果98%的时间是用于GC,且可用的Heap size 不足2%的时候将抛出此异常信息。该种情况一般JVM会反复GC去释放内存。

        要解决Java heap space OutOfMemory的问题,一般需要对Dump文件(内存快照)进行分析,分析Dump一般需要借助内存映像分析工具(如 Eclipse Memory Analyzer,JProfiler等),下图是JProfiler打开的一个dump文件。可以通过该类工具查看java堆中对象具体信息,从Instance Count(实例数量),Size,retained size(retained heap是指对象自己本身的shallow heap的大小加上对象所引用的对象的大小)几个方面基本可以确定堆中哪一类对象发生了泄露,再定位具体原因。

        Dump文件打印方法:

         1.jmap -dump:live,file=${fileName} ${pid}

         2.参数  -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${目录}    可以让虚拟机在出现内存溢出时Dump出当前内存的快照


        堆泄露要先判断出是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

        如果是内存泄漏,可以通过工具进一步查看出对象的GC Roots的引用链(下图是Eclipse Memory Analyzer的一个对象的GC Roots引用链),根据对象的内容,类型和GC Roots引用链一般就可以精确地定位到泄露代码的位置,再对代码做分析找出泄露的地方。

         如果不是内存泄漏,那就说明堆内对象都是存活需使用的,可能存在内存溢出的问题,这时就要检查虚拟机的堆参数设置的是否合理(-Xmx和-Xms)是否需要增大配置。还要检查代码是否可以优化,某些对象是否存在生命周期过长,对象过大的问题,作者曾经遇到过堆内一个List对象retained size达到700MB的情况,一个对象就造成了堆溢出,尝试优化代码也可以减少内存消耗。


2.Non-Heap泄露(非堆)

    Non-Heap泄露不在堆内,dump文件一般无法看出这块泄露的原因。

   1)方法区和运行常量池溢出

    运行常量池是方法区的一部分,JDK1.7开始逐步开始去永久代,因此运行时常量池也从堆内的永久代转移到了堆外的方法区里。运行时常量池也是方法区的一部分,同样受到方法区内存的限制,常量池无法申请到内存是会抛出OutOfMemoryError异常。

    JDK1.7以前的

    java.lang.OutOfMemoryError: PermGen space

    JDK1.7后的

    java.lang.OutOfMemoryError: Metaspace space

   两种异常都是方法区溢出。

   jcmd  ${pid}  VM.native_memory summary scale=MB命令可以查看JVM Native Memory ,通过该命令可以看出metaspace占用内存情况。

(注意该命令需在启动时添加参数-XX:NativeMemoryTracking=summary,打开NMT会带来5%-10%的性能损耗,生产环境不建议开启)

     

    jmap -heap ${pid}命令可以查看jvm Heap Configuration  ,从下图看出JVM默认的MaxMetaspaceSize的默认值是非常大的,Metaspace最大值取决于机器的最大内存,如果代码中有使用动态生成class的技术,建议设置 -XX:MaxMetaspaceSize 参数,不然可能会发生进程占用内存过多的情况。

     .

如果发生方法区溢出可以在JVM参数加上  -XX:+TraceClassLoading -XX:+TraceClassUnloading 记录下类的加载和卸载情况。

使用arthas工具sc命令查看JVM已加载的类信息,sc -d  ${Class} 可以查看class详细信息包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。 如果一个类被多个ClassLoader所加载,则会出现多次。使用arthas可以帮助排查Metaspace溢出的原因。再根据类加载详细信息排查代码,确定是否需要增加MaxMetaspaceSize还是优化类加载相关的代码。

arthas下载:https://github.com/alibaba/arthas/releases

arthas教程:https://blog.csdn.net/qq_43692950/article/details/122686329


   2)虚拟机栈和本地方法区溢出

    StackOverflowError

    栈溢出报错有详细的堆栈信息,出现该异常的原因是函数调用栈太深了,需检查代码是否存在线程栈深度过大(比如循环调用方法而无法退出)或者本地变量数量过多,变量过大的情况。一般情况下JVM默认的  -Xss(虚拟机栈大小)是足够使用的,出现该问题优先排查代码问题,如果确认栈大小不够可调整  -Xss  参数大小。

   java.lang.OutOfMemoryError: unable to create new native thread

    创建线程过多导致的内存溢出,这个异常一般由于两个原因导致的:

    1)内存空间不足以满足创建线程所需的stack size
     virtual memory < stack size*the number of threads
    2)线程数已达到操作系统的上限

    解决方法可参考:https://www.jianshu.com/p/b0df25bae79d


3.Non-JVM Memory泄露(本机直接内存溢出)

     同样适用jcmd  ${pid}  VM.native_memory summary scale=MB命令查看NMT数据,可以看出整个进程的内存使用情况。

     -XX:MaxDirectMemorySize可以指定DirectMemory(直接内存大小),如果不设置该值则默认与Java堆最大值(-Xmx)一样。DirectMemory泄露在Heap  Dump文件中看不出明显的异常,如果发现程序OOM后Dump文件没有达到设置的-Xmx值,metaspace是用大小也没有达到 -XX:MaxMetaspaceSize 值,那么就可以考虑是不是DirectMemory发生了泄露。

     pmap 命令可查看进程的内存映像信息

     

    启动参数 -verbose:jni 查看本地方法调用的情况。

   

    DirectMemory泄露通过JVM工具可能看不出太多的信息,搭配perf 查看native调用可以找到相应的调用函数。

    参考:https://blog.csdn.net/qq_33594101/article/details/120656528

    还可以使用gdb分析内存块内容

    参考:https://blog.csdn.net/qianshangding0708/article/details/100978730

     直接内存泄露问题定位是比较困难的,后续如果遇到了会补充上一些详细定位过程。


小节

内存泄露是java应用比较难处理的问题,往往受生产环境限制,问题不易复现,现象多种多样,作者处理这类问题最多的时间都花费在了问题复现上,复现出问题后解决反而不会花费太多时间,个人建议遇到内存泄漏问题一定要复现出一样的场景,问题复现后再根据错误和异常信息去做定位。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。