zoukankan      html  css  js  c++  java
  • ConcurrentHashMap内存泄漏问题

    问题背景

    上周,同事写了一段ConcurrentHashMap的测试代码,说往map里放了32个元素就内存溢出了,我大致看了一下他的代码及运行的jvm参数,觉得很奇怪,于是就自己捣鼓了一下。首先上一段代码:

     1 public class MapTest {
     2 
     3     public static void main(String[] args) {
     4         System.out.println("Before allocate map, free memory is " + Runtime.getRuntime().freeMemory()/(1024*1024) + "M");
     5         Map<String, String> map = new ConcurrentHashMap<String, String>(2000000000);
     6         System.out.println("After allocate map, free memory is " + Runtime.getRuntime().freeMemory()/(1024*1024) + "M");
     7 
     8         int i = 0;
     9         try {
    10             while (i < 1000000) {
    11                 System.out.println("Before put the " + (i + 1) + " element, free memory is " + Runtime.getRuntime().freeMemory()/(1024*1024) + "M");
    12                 map.put(String.valueOf(i), String.valueOf(i));
    13                 System.out.println("After put the " + (i + 1) + " element, free memory is " + Runtime.getRuntime().freeMemory()/(1024*1024) + "M");
    14                 i++;
    15             }
    16         } catch (Exception e) {
    17             e.printStackTrace();
    18         } catch (Throwable t) {
    19             t.printStackTrace();
    20         } finally {
    21             System.out.println("map size is " + map.size());
    22         }
    23     }
    24 
    25 }

    执行时加上jvm执行参数 -Xms512m -Xmx512m ,执行结果:

    Before allocate map, free memory is 483M
    After allocate map, free memory is 227M
    Before put 0 element, free memory is 227M
    java.lang.OutOfMemoryError: Java heap space
        at java.util.concurrent.ConcurrentHashMap.ensureSegment(ConcurrentHashMap.java:746)
        at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1129)
        at com.test.MapTest.main(MapTest.java:21)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:606)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
    map size is 0

      最开始的代码是没有加入一些日志打印的,当时就很奇怪,为什么只往map里放一个元素就报了OutOfMemoryError了。

      于是就加了上述打印日志,发现在创建map的时候已经占用了二百多兆的空间,然后往里面put一个元素,put前都还有二百多兆,put时就报了OutOfMemoryError, 那就更奇怪了,初始化map的时候会占用一定空间,可以理解,但是只是往里面put一个很小的元素,为什么就会OutOfMemoryError呢?

    排查过程

      1、第一步,将第一段代码中的Map<String, String> map = new ConcurrentHashMap<String, String>(2000000000);修改为Map<String, String> map = new ConcurrentHashMap<String, String>(2000000000);,这次运行正常。(没有找到问题根因,但是以后使用ConcurrentHashMap得注意:1、尽量不初始化;2、如果需要初始化,尽量给一个比较合适的值)

      2、第二步,执行时加上jvm参数-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:gc.hprof,后面两个参数主要作用是当程序内存溢出时会打印堆快照文件,已便于我们来分析内存。生成gc.hprof文件后,采用Eclipse Memory Analyzer来分析,结果如下图,可见ConcurrentHashMap中的HashEntry占用了大量内存(关于Eclipse Memory Analyzer分析待补充)。此时只是找到了哪个对象占用空间,但是为什么为出现这种情况不得而知。

     

      3、第三步,分析ConcurrentHashMap源代码,首先,了解一下ConcurrentHashMap的结构,它是由多个Segment组成(每个Segment拥有一把锁,也是ConcurrentHashMap线程安全的保证,不是本文讨论的重点),每个Segment由一个HashEntry数组组成,出问题就在这个HashEntry上面。

      4、第四步,查看ConcurrentHashMap的初始化方法,可以看出第27行,初始化了Segment[0]的HashEntry数组,数组的长度为cap值,这个值为67108864

        cap的计算过程(可以针对于初始化过程进行调试)

          1)initialCapacity为2,000,000,000,而MAXIMUM_CAPACITY的默认值(也即ConcurrentHashMap支持的最大值是1<<30,即230=1,073,741,824),initialCapacity的值大于MAXIMUM_CAPACITY,即initialCapacity=1,073,741,824

          2)c的值计算为 initialCapacity/ssize=67108864

          3)cap为 大于等于c的第一个2的n次方数 也即67108864

     1     public ConcurrentHashMap(int initialCapacity,
     2                              float loadFactor, int concurrencyLevel) {
     3         if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
     4             throw new IllegalArgumentException();
     5         if (concurrencyLevel > MAX_SEGMENTS)
     6             concurrencyLevel = MAX_SEGMENTS;
     7         // Find power-of-two sizes best matching arguments
     8         int sshift = 0;
     9         int ssize = 1;
    10         while (ssize < concurrencyLevel) {
    11             ++sshift;
    12             ssize <<= 1;
    13         }
    14         this.segmentShift = 32 - sshift;
    15         this.segmentMask = ssize - 1;
    16         if (initialCapacity > MAXIMUM_CAPACITY)
    17             initialCapacity = MAXIMUM_CAPACITY;
    18         int c = initialCapacity / ssize;
    19         if (c * ssize < initialCapacity)
    20             ++c;
    21         int cap = MIN_SEGMENT_TABLE_CAPACITY;
    22         while (cap < c)
    23             cap <<= 1;
    24         // create segments and segments[0]
    25         Segment<K,V> s0 =
    26             new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
    27                              (HashEntry<K,V>[])new HashEntry[cap]);
    28         Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    29         UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    30         this.segments = ss;
    31     }

      5、第五步,查看ConcurrentHashMap的put方法,可以看出在put一个元素的时候,

        1)计算当前key的hash值,查看当前key所在的semgment有没有初始化,若果有,则执行后续的put操作。

        2)如果没有去执行ensureSement()方法,而在ensureSement()方法中又会初始化了一个HashEntry数组,数组的长度和第一个初始化的Segment的HashEntry的长度一致,可以从代码行的18、19行看出。

     1     public V put(K key, V value) {
     2         Segment<K,V> s;
     3         if (value == null)
     4             throw new NullPointerException();
     5         int hash = hash(key);
     6         int j = (hash >>> segmentShift) & segmentMask;
     7         if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
     8              (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
     9             s = ensureSegment(j);
    10         return s.put(key, hash, value, false);
    11     }
    12 
    13     private Segment<K,V> ensureSegment(int k) {
    14         final Segment<K,V>[] ss = this.segments;
    15         long u = (k << SSHIFT) + SBASE; // raw offset
    16         Segment<K,V> seg;
    17         if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
    18             Segment<K,V> proto = ss[0]; // use segment 0 as prototype
    19             int cap = proto.table.length;
    20             float lf = proto.loadFactor;
    21             int threshold = (int)(cap * lf);
    22             HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
    23             if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
    24                 == null) { // recheck
    25                 Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
    26                 while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
    27                        == null) {
    28                     if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
    29                         break;
    30                 }
    31             }
    32         }
    33         return seg;
    34     }

      6、这个时候能定位到是因为put一个元素的时候创建了一个长度为67103684的HashEntry数组,而这个HashEntry数组会占用67108864*4byte=256M,和上面的测试结果能对应起来。为什么数组会占用这么大的空间,很多同学可能会有疑问,那来看一下数组的初始化  而数组初始化会在堆内存创建一个HashEntry引用的数组,且长度为67103684,而每个HashEntry引用(所引用的对象都为null)都占用4个字节(参见http://www.importnew.com/18878.html)。

    问题总结

      1、ConcurrentHashMap初始化时要指定合理的初始化参数(当然本人做了一个小测试,指定初始化参数暂时没有发现性能上的提升,所以待各位看客自己来评估)

    后记

      这篇博客是本人的第一篇博客,当然有很多不足之处,如果有纰漏欢迎各位大神指出,谢谢各位!

  • 相关阅读:
    测试候选区
    This is my new post
    发布到首页候选区
    nb
    []
    隐藏列(gridview遍历)
    交叉表、行列转换和交叉查询经典
    数据库设计中的14个技巧
    jQuery操作表格,table tr td,单元格
    不愿将多种编程语言杂糅在一起?可能你还没意识到而已
  • 原文地址:https://www.cnblogs.com/coder-yoyo/p/6215175.html
Copyright © 2011-2022 走看看