• Java堆外内存之六:堆外内存溢出问题排查


     一、堆外内存组成

    通常JVM的参数我们会配置

    -Xms 堆初始内存
    -Xmx 堆最大内存
    -XX:+UseG1GC/CMS 垃圾回收器
    -XX:+DisableExplicitGC 禁止显示GC
    -XX:MaxDirectMemorySize 设置最大堆外内存,默认是-xmx-survivor,也就是基本上和-xmx大小相等
    -Xss:每个线程的堆栈大小,默认1M
    -Xmn: 年轻代大小(eden区+2 survivor)
    -XX:newRatio: 4 年轻代与老年代1:4
    -XX:survivorRatio: 8Eden区与survivor大小比值

    java整个进程占用的内存:
    - 堆内存
    - metaspace(堆内) JDK8使用metaspace来替代了permsize:永久代大小
    - 堆外内存使用
    - 线程栈空间

    堆外内存回收: 堆外内存的回收是通过system.gc()来的,依赖于目前的gc机制。
    通常是通过DirectByteBuffer对象来分配堆外内存,gc的时候就是判断这个对象是否被引用,来决定是否回收。

    二、堆外内存参数配置

    -XX:InitialCodeCacheSize=64M
    -XX:CodeCacheExpansionSize=1M
    -XX:CodeCacheMinimumFreeSpace=1M
    -XX:ReservedCodeCacheSize=200M
    -XX:MinMetaspaceExpansion=1M
    -XX:MaxMetaspaceExpansion=8M
    -XX:MaxDirectMemorySize=96M
    -XX:CompressedClassSpaceSize=256M

     三、问题排查

    3.1、首先确认堆占用

    1、用jmap,jmap 查看heap内存使用情况

    jmap -heap pid

    可以查看到MetaspaceSize,CompressedClassSpaceSize,MaxMetaSize
    jmap和jdk版本有关系,有些jdk版本会查看不到内存信息,可以使用jstat来查看统计信息

    2、jstat 收集统计信息

    jstat -gc pid 1000
    S0C/S0U            S1C/S1U           EC/EU    CCSC/CCSU                  YGC/YGCT          FGC/FCGT         GCT
    survivor0容量和使用 survivor1容量和使用 Eden jdk8是meta,以前应该是PC,PC young gc次数和耗时 full gc次数和耗时 total gc时间

    如果能排除掉heap的问题,就要分析堆外内存情况了。

    3.2、分析堆外情况

    NMT(native memory tracking)
    使用
    在JVM参数中添加 -XX:NativeMemoryTracking=[off | summary | detail]

    -XX:NativeMemoryTracking=detail

    在JVM运行过程中,使用jcmd获取相关信息
    jcmd pid VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]

    jcmd pid VM.native_memory detail

    baseline个基准,之后会输出diff参数,来和这个基线版本进行比较,可以两次的内存差

    NMT报告会显示内存使用情况

    类别                  含义
    Java Heap        堆大小
    Thread              线程
    Thread Stack    线程栈

    NMT可以得到线程栈大小,排除栈空间影响。

    pmap 查看进程内存地址空间

    pmap -x pid | sort xx

    可以结合pmap,和nmt得到内存地址空间。和堆外占用情况了。

    接下来需要做的就是分析堆外内存的内容了。

    gdb dump查看内存空间内容

    gdb dump查看内存空间内容

    (gdb) dump binary memory ./file BEGIN_ADDRESS END_ADDRESS

    将内存内容dump到文件中,就可以查看到文件中的内容了。
    但是这种方式不直观,所以可以使用其他工具

    gperf
    google的,使用gperf2.5即可,网上很多安装都说一定要安装libunwind,其实都是瞎抄抄,老版本确实需要,2.5的版本不需要了。

    https://blog.csdn.net/unix21/article/details/79161250
    另外一个注意点就是虽然heap文件只有1M,但是可以分析出堆外内存的大小。
    不过我在实际使用过程中,gperf并没有分析出实际的堆外内存情况,通过pmap可以看出堆外内存占用有几个G,但是gperf始终只有200M

    Jemalloc
    https://github.com/jemalloc/jemalloc/releases
    安装

    ./configurate –enable-prof
    make
    sudo make install
    配置

    export LD_PRELOAD=/usr/local/lib/libjemalloc.so
    export MALLOC_CONF=prof:true,lg_prof_interval:31,lg_prof_sample:17,prof_prefix:/output/jeprof
    https://github.com/jemalloc/jemalloc/wiki/Getting-Started

    环境:基于BS的点子考试系统,为了发现客户端能实时地从服务端接收考试数据,系统使用了逆向AJAX技术(也称Comet或Server Side Push),选用CometD1.1.1作为服务端推送框架,服务器是Jetty7.1.4,硬件为一台普通PC机,Core i5 CPU,

            4G内存,运行32位Windows操作系统。

    说明:测试期间发现服务端不定时抛出内存溢出异常,服务器不一定每次都会出现异常,但是假如正式考试时奔溃一次,那估计整场考试都会全乱套,网站管理员尝试过把堆开到最大,32位系统最多到1.6GB基本无法再加大了,而且开大量也基本没效果,抛出

            内存溢出异常好像更加繁琐了。加入-XX:+HeapDumpOnOutOfMemoryError,居然也没有任何反应,抛出内存溢出异常时什么文件都没产生。无奈之下只好挂着jstat使劲盯屏幕,发现GC并不频繁,Eden区,Survivor区,老年代及拥挤代内存全部

           表示"情绪稳定,压力不大",但是照样不停的抛出内存溢出异常,管理员鸭梨很大。最后,在内存溢出后从系统日志中找到异常堆栈。

    分析:大家都知道操作系统对每个进程能管理的内存是有限的,这台服务器使用的32位Windows平台的限制是2GB,其中给了Java堆1.6GB,而Direct Memory 并不算在1.6GB的堆之内,因此它只能在剩余的0.4GB空间分出一部分。在此应用中导致内 

            存溢出的关键是:垃圾收集进行时,虚拟机虽然会对Direct Memory进行回收,但是Direct Memory 却不能像新生代和老年代那样,发现空间不足了就通知收集器进行垃圾回收,他只能等到抛出内存溢出异常时,先catch掉,再在catch块里面“大喊”

            “System.gc”.要是虚拟机还是不听(如:打开了-XX:+DisableExplicitGC开关),那就只能眼睁睁地看着堆中还有许多空闲内存,自己却不得不抛出内存异常了。而本案例中使用的Comet1.1.1框架,正好有大量的NIO操作需要用到Direct Memory。

    总结:从实践经验来看,除了java堆和永久代之外,我们注意到下面这些区域也会占用较多的内存,这里所有的内存总和会受到操作系统进程最大内存的限制:

            1.Direct Memory:可以通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或OutOfMemoryError:Direct buffer memory。

            2.线程堆栈:可通过-Xss调整大小内存不足时抛出StackoverflowErroe(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。

            3.Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB的内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。

            4.JNI代码:如果代码中使用JNI调用本地库,那么本地库使用内存也不在堆中

            5.虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。

  • 相关阅读:
    LeetCode偶尔一题 —— 617. 合并二叉树
    《剑指offer》 —— 链表中倒数第k个节点
    《剑指offer》 —— 青蛙跳台阶问题
    《剑指offer》—— 二维数组中的查找
    《剑指offer》—— 替换空格
    《剑指offer》—— 合并两个排序的链表
    《剑指offer》—— 礼物的最大价值
    生成Nuget 源代码包来重用你的Asp.net MVC代码
    Pro ASP.Net Core MVC 6th 第四章
    Pro ASP.NET Core MVC 6th 第三章
  • 原文地址:https://www.cnblogs.com/duanxz/p/6089421.html
Copyright © 2020-2023  润新知