一次内存泄露排查小结

文章目录

        • 问题重现
        • 排查思路
        • 优化方法
        • 知识小结

问题重现

前段时间给DSP(实际竞价广告投放系统)系统开发了一个前置数据去重处理服务,开始两天没有问题,但是第三天去看数据,发现进程不在。

查看阿里云机器负载,发现前两天内存持续在高位,初步怀疑内存溢出,导致异常退出。

排查思路

  • 1.确定代码中有大量对象占用的地方,着重排查:因为堆内存是JVM中内存占用最大的一块。

  • 2.多线程代码排查:多线程下容易出现隐蔽的bug

  • 3.是否是机器本身内存太小,有其他进程占用大量内存。

关于思路1:要清楚使用的jdk版本,以及相应版本对应的JVM内存的分布情况。

要清楚以下几点:

  • 首先JVM内存受限于实际的最大物理内存,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体到JVM操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系 统下为2G-3G),而64bit以上的处理器就不会有限制了。

  • JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,

    默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;

    空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、 -Xmx相等以避免在每次GC后调整堆的大小。

  • JDK8后,方法区在元空间实现,元空间使用直接内存. 但是一般方法区占用内存并不会太大。

优化方法

如果进程已经挂掉,可以分析堆内存快照,具体设置方式参考下文。如果进程还在,可以查看当前的内存情况,排查问题:

  • 通过java自带的工具查看堆内存情况

    #查看堆内存使用情况
    jmap -heap pid
    
    Heap Usage:
    PS Young Generation
    Eden Space:
       capacity = 290455552 (277.0MB)
       used     = 290455552 (277.0MB)
       free     = 0 (0.0MB)
       100.0% used
    From Space:
       capacity = 20447232 (19.5MB)
       used     = 14548992 (13.875MB)
       free     = 5898240 (5.625MB)
       71.15384615384616% used
    To Space:
       capacity = 20447232 (19.5MB)
       used     = 0 (0.0MB)
       free     = 20447232 (19.5MB)
       0.0% used
    PS Old Generation
       capacity = 420478976 (401.0MB)
       used     = 364028688 (347.16481018066406MB)
       free     = 56450288 (53.83518981933594MB)
       86.5747656310883% used
    

    这里需要了解JVM垃圾回收器的分代逻辑,简单来说就是新创建的对象放在Eden Space, 之后将存活的对象放入From Space和To Space区域,如果超过一定次数,则放入Old Generation,所以如果老年代如果空间一直上升,而没有下降,则说明有内存泄露,导致对象一直没有释放,就要重点排查。具体可以结合dump文件。

  • 通过java自带工具jmap查看class实例等信息,作用类似于查看dump文件。

    #输出当前class的实例数目、内存占用、类全名信息。
    [root@iZ0xicvgqrspr6w1sxojjeZ tmp]# jmap -histo:live 6104 | head -n 20
    
     num     #instances         #bytes  class name
    ----------------------------------------------
       1:         66065       10737048  [C
       2:         12932        9308496  [B
       3:         12049        4969152  [I
       4:         28079        2562408  [Ljava.lang.Object;
       5:         65697        1576728  java.lang.String
       6:         48265        1544480  java.util.concurrent.ConcurrentHashMap$Node
       7:         39782        1273024  java.util.HashMap$Node
       8:         11210        1239576  java.lang.Class
       9:          1036        1064000  [Lio.netty.util.Recycler$DefaultHandle;
      10:         15295         611800  java.security.cert.TrustAnchor
      11:          5817         511896  java.lang.reflect.Method
      12:          1602         493712  [Ljava.util.concurrent.ConcurrentHashMap$Node;
      13:          3865         485632  [Ljava.util.HashMap$Node;
      14:         20168         484032  java.util.ArrayList
      15:         27529         440464  java.lang.Object
      16:          8681         347240  java.util.LinkedHashMap$Entry
      17:           390         255840  io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueue
    
  • 通过jstat命令查看gc情况

    如果FGC次数比较频繁,则说明内存吃紧,要排查是内存本身较小,还是内存泄露。

    [root@iZ0xicvgqrspr6w1sxojjeZ work]# jstat -gc 28686
     S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
    8192.0 7680.0 4544.0  0.0   770560.0 497923.9 1310720.0   852555.0  65624.0 62895.9 8024.0 7467.0 146565 2120.377  132    84.347 2204.724
    

    S0C:年轻代中第一个幸存区的大小

    S1C:年轻代中第二个幸存区的大小

    S0U:年轻代中第一个幸存区的使用大小

    S1U:年轻代中第二个幸存区的使用大小

    EC:年轻代中伊甸园区的大小

    EU:年轻代中伊甸园区的使用大小

    OC:老年代大小

    OU:老年代使用大小

    MC:方法区大小

    MU:方法区使用大小

    CCSC:压缩类空间大小

    CCSU:压缩类空间使用大小

    YGC:年轻代gc次数

    YGCT:年轻代消耗时间

    FGC:老年代gc次数

    FGCT:老年代gc消耗时间

    GCT:gc消耗总时间

  • 查看内存占用靠前的java进程

    top -o %MEM -b -n 1 | grep java | awk '{print "PID: "$1" \t MEM: "$6" \t %CPU: "$9"% \t %MEM: "$10"%"}'
    
  • 到处堆内存快照

    jmap -histo [进程id] > jmap.txt
    
  • 首先调整java运行时的相关参数,包括内存溢出时的堆内存快照。

    nohup java -Xmx2048m -Xms2048m -Xmn768m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -jar ${jar_name} --spring.profiles.active=prod-us --server.port=8089 >/dev/null 2>&1 &
    
    JVM参数 描述 默认 推荐
    -Xms Java堆内存的最小值,即初始值 OS内存1/64 OS内存一半
    -Xmx java堆内存最大值 OS内存1/4 OS内存一半
    -Xmn 堆内存新生代大小,扣除新生代
    就是老年代大小
    默认堆的1/3 sun推荐3/8
    -Xss 每个线程的栈内存大小 和jdk有关 512k~1M

    如果是本身内存设置过小,一般通过调整参数便可以解决。如果是其他问题,可以通过heapdump.hprof文件协助查看占用内存较大的对象分布,方便排查。

    根据这些信息,重点排查相关部位的代码,一般都能解决。之前我还遇到在使用线程池时,采用了Executors工具类去创建线程池,而用它创建线程池,默认的等待队列是无界队列,所以有可能导致创建大量等待线程,导致内存溢出(线程要占用资源)。正确的做法是使用下面这种方式,给一个有界队列作为参数。

    ThreadPoolExecutor threadPool = 
        new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
    	new LinkedBlockingQueue<>(2000), new DefaultThreadFactoryA(), new ThreadPoolExecutor.CallerRunsPolicy());
    

一次内存泄露排查小结_第1张图片

  • 查看java进程常用命令:

    #1.简洁信息:可以查看进程号
    jps
    #2.可查看进程号和jar包名称
    jcmd
    jps -lv
    #3.详细信息
    ps -ef|grep java
    
  • 查看系统内存情况:

    #Top命令 ,可以查看系统负载情况,内存占用,和进程的情况
    top - 18:59:23 up 5 days,  8:28,  2 users,  load average: 28.50, 30.68, 30.82
    Tasks:  84 total,   1 running,  83 sleeping,   0 stopped,   0 zombie
    %Cpu(s):  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    KiB Mem :  8008928 total,  3534996 free,  3400088 used,  1073844 buff/cache
    KiB Swap:        0 total,        0 free,        0 used.  4355048 avail Mem 
    
      PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND            1150 root      10 -10  140104  18748  11464 S   1.3  0.2  90:48.01 AliYunDun          947 root      20   0 1354476  21268   8416 S   0.3  0.3  29:38.53 /usr/local/clou      1 root      20   0   43584   3976   2620 S   0.0  0.0   0:04.72 systemd                 2 root      20   0       0      0      0 S   0.0  0.0   0:00.00 kthreadd               4 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/0:0H           5 root      20   0       0      0      0 S   0.0  0.0   0:00.00 kworker/u4:0           6 root      20   0       0      0      0 S   0.0  0.0   0:35.24 ksoftirqd/0           7 root      rt   0       0      0      0 S   0.0  0.0   0:00.44 migration/0           8 root      20   0       0      0      0 S   0.0  0.0   0:00.00 rcu_bh                 9 root      20   0       0      0      0 S   0.0  0.0   1:27.46 rcu_sched        
    

    只想查看java进程的情况:

    [root@iZ0xicvgqrspr6w1sxojjeZ ~]# top -o %MEM -b -n 1 | grep java | awk '{print "PID: "$1" \t MEM: "$6" \t %CPU: "$9"% \t %MEM: "$10"%"}'
    PID: 1592        MEM: 2.4g       %CPU: 0.0%      %MEM: 31.4%
    PID: 1506        MEM: 383856     %CPU: 0.0%      %MEM: 4.8%
    

知识小结

内存泄露、JVM调优等问题一般比较隐蔽,首先要对jvm的相关知识有一个大体认知,其次要熟悉java中相关工具类的正确用法,最后就是要有给力的工具,比如jprofiler等等。

你可能感兴趣的:(并发编程,jvm,java,linux)