设为首页 收藏本站
查看: 966|回复: 0

[经验分享] tomcat频繁内存溢出,但是查看jvm并没有不够用,怎么回事?请来设置下启动参数吧

[复制链接]

尚未签到

发表于 2017-2-11 07:44:53 | 显示全部楼层 |阅读模式
XSocket内存泄漏问题深度分析


  大概一个月前在一个数据迁移的过程中,在数据迁移到900多W的时候程序崩溃了,系统最后记录的日志是这样的:
 Exception in thread "xDispatcher#CLIENT" java.lang.OutOfMemoryError
        at sun.misc.Unsafe.allocateMemory(Native Method)
        at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99)
        at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
        at org.xsocket.connection.spi.AbstractMemoryManager.newBuffer(AbstractMemoryManager.java:219)
        at.......
  从中不难看出这是xsocket的内存管理层程序通过JVM的nio创建DirectByteBuffer时抛出了错误。在这里先要说明一下的是,当时的系统使用的xsocket2.0版,2.0版连接读取数据默认就是使用DirectByteBuffer的,目前2.4.X版已经默认都改为使用ByteBuffer,而不再是DirectByteBuffer了。
DirectByteBuffer是由jvm调用jni程序在操作系统内存中分配的空间的,不需要占用JVM的Heap Size。当程序需要读取Big Size或者Huge Size数据的时候,使用DirectByteBuffer优势尤其明显。但使用DirectByteBuffer会产生一个问题,当JVM的空间还没满但系统内存空间已经被消耗的差不多的时候,gc如何被触发呢。这个问题在一个叫Harmony的开源Java SE项目的mail list中曾进行过热烈的讨论。参考资料1中有当时讨论过程的链接。
在跟踪JVM的一些源码后,发现,很庆幸,sun的jvm已经解决这个问题了。但是对于尚不清楚其内存处理机制的开发人员来说。在使用DirectByteBuffer是还是很可能把系统置身于莫大的风险中。而很不幸这个问题隐藏的相当的隐秘,对于不明就里的人,最后可能只好采取定时重启系统或者在系统中设定一些条件显式调用gc来曲线解决资源释放的问题。

题外话说了不少,下面我们直接从JVM的一些代码片段去分析文章最开始的Exception是在什么条件下引发的。首先我们来看看DirectByteBuffer的构建函数代码:
     DirectByteBuffer(int cap) {
        super(-1, 0, cap, cap, false);
        Bits.reserveMemory(cap);
        int ps = Bits.pageSize();
        long base = 0;
        try {
            base = unsafe.allocateMemory(cap + ps);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(cap);
            throw x;
        }
        unsafe.setMemory(base, cap + ps, (byte) 0);
        if (base % ps != 0) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, cap));
    }
  注意以上片段中红色加亮的部分,然后我们再来看看Bits.reserveMemory这个方法的代码:
     // -- Direct memory management --

    // A user-settable upper limit on the maximum amount of allocatable
    // direct buffer memory.  This value may be changed during VM
    // initialization if it is launched with "-XX:MaxDirectMemorySize=<size>".
    private static volatile long maxMemory = VM.maxDirectMemory();
    private static volatile long reservedMemory = 0;
    private static boolean memoryLimitSet = false;

    // These methods should be called whenever direct memory is allocated or
    // freed.  They allow the user to control the amount of direct memory
    // which a process may access.  All sizes are specified in bytes.
    static void reserveMemory(long size) {
        synchronized (Bits.class) {
            if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
            }
            if (size <= maxMemory - reservedMemory) {
            reservedMemory += size;
            return;
            }
        }

        System.gc();
        try {
            Thread.sleep(100);
        } catch (InterruptedException x) {
            // Restore interrupt status
            Thread.currentThread().interrupt();
        }
        synchronized (Bits.class) {
            if (reservedMemory + size > maxMemory)
            throw new OutOfMemoryError("Direct buffer memory");
            reservedMemory += size;
        }
    }
  从以上代码我们可以看到,JVM在通过DirectByteBuffer使用OS内存时(无论是分配还是释放),是有统计的,通过跟可使用的最大OS内存(VM.maxDirectMemory())作比较,如果不够用,那显式调用gc,如果经过gc后还是没有足够的空分配内存,那么从Bit抛出Exception。注意:这一步并未真正的像OS申请内存,只是VM通过计算得出的结论。而真正想OS申请内存是在 unsafe.allocateMemory这个方法里面通过JNI实现的。显然,文章最初的Exception是由Unsafe抛出而不是Bit抛出。也就是说JVM认为OS还有足够的可分配内存,而当JVM真正向OS申请内存分配的时候却出错了。那么接下来我们就得看看,这个max direct memory值是如何设定的。

现在我们看看VM.java的代码:
   private static long directMemory = 67108864L;
  ...
  public static long maxDirectMemory()
  {
    if (booted)
      return directMemory;
    Properties localProperties = System.getProperties();
    String str = (String)localProperties.remove("sun.nio.MaxDirectMemorySize");
    System.setProperties(localProperties);
    if (str != null)
      if (str.equals("-1"))
      {
        directMemory = Runtime.getRuntime().maxMemory();
      } else {
        long l = Long.parseLong(str);
        if (l > -1L)
          directMemory = l;
      }
    return directMemory;
  }
  可以看出来,max direct memory可以有三种值:
1)默认值,64M;
2)maxMemory,也就是通过-Xmx设定的值,默认也是64M;
3)通过-XX:MaxDirectMemorySize=<size>指定的值;

问题到这里基本就是水落石出了,当时系统启动的时候设定-Xmx2048M,未指定MaxDirectMemorySize,也就是说max directmemory被认为是2048M,整个系统的物理内存为4G,除掉系统进程占用的内存,剩下的物理内存加上swap空间也就接近3G。设想JVM的heap size占用了1.5G,direct memory使用了1.5G,这时候程序申请一200M的direct内存,在这种情况下无论是JVMheap size还是direct memory不满足触发gc的条件,于是jvm向os申请分配内存,OS无可分配的内存了就会抛出OOM错误。

补充说明一下:在OS已经把物理内存+Swap都耗光都不足够分配内存空间的时候,不同OS可能会有不同的表现。LInux的内核有可能会尝试努力腾出更多的内存空间。可能会杀掉某些进程。(这是参考资料一中Ivan Volosyuk所提出来的问题)。而我在使用以下程序进行测试时,出现整个OS系统假死的状况,过一段时间回复过来了。但整个过程可以肯定的一件事是gc始终没有被触发到。

以下是我用来验证我的以上分析的测试程序:
 import java.lang.management.ManagementFactory;
import java.nio.ByteBuffer;
import java.util.logging.Logger;

import com.sun.management.OperatingSystemMXBean;

public class TestMemoryLeak {
    private static Logger logger = Logger.getAnonymousLogger();

    public static void main(String[] args) throws Exception{
        OperatingSystemMXBean osmb = (OperatingSystemMXBean) ManagementFactory
                .getOperatingSystemMXBean();
        System.out.println("Total physic memory:"
                + osmb.getTotalPhysicalMemorySize() / 1024 / 1024 + "MB");
        System.out.println("Free physic memory:"
                + osmb.getFreePhysicalMemorySize() / 1024 / 1024 + "MB");
        System.out.println("Max memory:" + Runtime.getRuntime().maxMemory());
        System.out
                .println("Total memory:" + Runtime.getRuntime().totalMemory());
        System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
        System.out.println("====================================");
        
        //testDirectByteBuffer();
        testByteBuffer();
        
        System.out.println("====================================");
        System.out.println("Free physic memory:"
                + osmb.getFreePhysicalMemorySize() / 1024 / 1024 + "MB");
    }
   
    public static void testDirectByteBuffer(){
        try{
            ByteBuffer bb1 = ByteBuffer.allocateDirect(1024 * 100000 * 4);
            bb1 = null;
            System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
            ByteBuffer bb2 = ByteBuffer.allocateDirect(1024 * 100000 * 5);
            bb2 = null;
            System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
            //System.gc();
            //pause expect for gc
            Thread.sleep(1000*6);
            ByteBuffer bb3 = ByteBuffer.allocateDirect(1024 * 100000 * 8);
            System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());    
        }catch(Exception e){
            e.printStackTrace();
        }
    }   
    public static void testByteBuffer(){
        try{
            ByteBuffer bb1 = ByteBuffer.allocate(1024 * 100000 * 4);
            bb1 = ByteBuffer.allocate(1024 * 10 * 4);
            System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
            ByteBuffer bb2 = ByteBuffer.allocate(1024 * 100000 * 3);
            bb1 = ByteBuffer.allocate(1024 * 10 * 3);
            System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
            //System.gc();
            //pause expect for gc
            Thread.sleep(1000*6);
            ByteBuffer bb3 = ByteBuffer.allocate(1024 * 100000 * 6);
            System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());    
        }catch(Exception e){
            e.printStackTrace();
        }        
    }

}
  程序启动需用如下命令:
java -verbose:gc -Xms64M -Xmx512M -XX:MaxDirectMemorySize=1000M TestMemoryLeak
-verbose:gc用于开启gc运行情况的输出,可以帮助我们了解jvm垃圾收集的运作情况;
其他参数值应该你机器的实际情况设定。

最后我想总结的是,当我们在使用DirectByteBuffer的时候一定要注意:
1)谨慎设定JVM运行参数,最好用-XX:MaxDirectMemorySize进行设定,否则你就得清楚你设定的mx不单单制定了heap size的最大值,它同时也是direct memory的最大值;
2)在密集型访问中(例如数据迁移工具)适当增加对gc的显式调用,保证资源的释放;

参考资料:
[VM]How to trigue GC while free native memory is low.

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-340407-1-1.html 上篇帖子: publish to tomcat v5.0 server @ localhost...:performing tasks 下篇帖子: S2SH部署在Tomcat中可以,但部署在weblogic10.3中就报错
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表