Java 内存泄漏分析和对内存设置

goodboy 2年前
   <p>为了判断 Java 中是否有内存泄漏,我们首先必须了解 Java 是如何管理内存的。下面我们先给出一个简单的内存泄漏的例子,在这个例子中我们循环申请 Object 对象,并将所申请的对象放入一个 HashMap 中,如果我们仅仅释放引用本身,那么 HashMap 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。</p>    <pre>  <code class="language-java">HashMap mapObj = new HashMap()        public void myfun() {    String obj1 = new String("abcd");    mapObj.put(obj1, obj1);    ...    obj1 = null;   //此时 obj1 指向的物理内存没有释放,因为 hashmap 引用该地址   }  </code></pre>    <p>JVM 可以自动回收垃圾,但它只能回收满足条件的垃圾,有时需要们确保条件的满足。如果程序中,存在越来越多不在影响程序未来执行的对象(也就是不再需要的对象),而且这些对象和根对象之间存在引用路径,那么就发生了内存泄漏。</p>    <p>内存泄漏常发生在如下场景:</p>    <ul>     <li>全局容器类,对象不再需要时,忘记从容器中 remove</li>     <li>像 Runnable 对象等被 Java 虚拟机自身管理的对象,没有正确的释放渠道。Runnable 对象必须交给一个 Thread 去 run,否则该对象就永远不会消亡</li>    </ul>    <h2>1.1 Java 对象的 Size</h2>    <p>在 64 位的平台上,Java 对象的占用内存如下</p>    <table>     <thead>      <tr>       <th>类型</th>       <th>大小</th>      </tr>     </thead>     <tbody>      <tr>       <td>Object</td>       <td>16</td>      </tr>      <tr>       <td>Float</td>       <td>16</td>      </tr>      <tr>       <td>Double</td>       <td>24</td>      </tr>      <tr>       <td>Integer</td>       <td>16</td>      </tr>      <tr>       <td>Long</td>       <td>24</td>      </tr>     </tbody>    </table>    <h2>1.2 对象及其引用</h2>    <p>为了说明对象和引用,我们先定义一个简单的类</p>    <pre>  <code class="language-java">class Person {   String name;   int age;  }  </code></pre>    <p>Person p1 = new Person() 包含如下几个动作</p>    <ol>     <li>右边的 new Person 在堆空间分配一块内存,创建一个 Person 类对象</li>     <li>末尾的 () 意味着创建对象之后,立即调用构造函数,进行初始化</li>     <li>左边的 Person p1 创建了一个引用变量,所谓引用变量,就是后来用于指向 Person 类示例的引用</li>     <li>= 符号使刚刚创建的对象引用指向刚刚创建的对象<br> 上面的代码如下所示:</li>    </ol>    <p><img src="https://simg.open-open.com/show/3153048e9abba9f15646332bfd72c67d.png"></p>    <p>如果再将对象赋值给 p2 的话,变成下面这样的</p>    <p><img src="https://simg.open-open.com/show/d606b44e4cf71eba64fd9ea3708bd3ff.png"></p>    <p>执行 p2 = new Person() 之后变成</p>    <p><img src="https://simg.open-open.com/show/e5e7ad31d53fec4d2ba540f3dde1abcd.png"></p>    <h2>1.3 虚拟机垃圾自动回收机制</h2>    <p>垃圾自动回收做两件事情:</p>    <ol>     <li>标记垃圾</li>     <li>清除垃圾</li>    </ol>    <p>标记过程现在主要使用 根可达性 分析(还有引用计数法等),清除之后可能会有一些小的内存快,所有还有压缩的过程。</p>    <p>下图中的灰色对象表示可以被回收的对象(根不可达)</p>    <p><img src="https://simg.open-open.com/show/8c6f3bf81a13d92ef1503a09849852e9.png"></p>    <p>哪些对象可以成为 根 呢? <a href="http://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html&cp=37_2_3" rel="nofollow,noindex">http://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html&cp=37_2_3</a></p>    <ul>     <li>没有被任何外部对象引用的栈上的对象</li>     <li>静态变量</li>     <li>JNI handler 包括全局和局部</li>     <li>系统 Class</li>     <li>存活着的监视器</li>    </ul>    <h2>2 内存泄漏的症状</h2>    <h2>2.1 为什么会发生 OOM 问题?</h2>    <p>内存不足会有三种情况:</p>    <ul>     <li>对内存不足</li>     <li>本地内存不足</li>     <li>Perm 内存不足</li>    </ul>    <p>发生 OOM 的时候,可以检查如下几个方面:</p>    <ul>     <li>应用程序的缓存功能</li>     <li>大量长期活动对象</li>     <li>对内存泄漏</li>     <li>本地内存泄漏</li>    </ul>    <h2>2.2 内存泄漏的症状</h2>    <p>内存泄漏一般会有如下几个症状:</p>    <ul>     <li>系统越来越慢,并且有 CPU 使用率过高</li>     <li>运行一段时间后,OOM</li>     <li>虚拟机 core dump</li>    </ul>    <h2>3 内存泄漏的定位和分析</h2>    <p>内存泄漏的分析并不复杂,但需要耐心,一般内存泄漏只能事后分析,而重现问题需要耐心。</p>    <h2>3.1 对内存泄漏定位</h2>    <p>当出现 java.lang.OutOfMemoryError: Java Heap Space 异常,就表示堆内存不足了。堆内存不足的原因有如下几种:</p>    <ul>     <li>堆内存设置太小</li>     <li>内存泄漏</li>     <li>设计不足,缓存了多余的数据</li>     <li>如果怀疑有内存泄漏,可以添加 -verbose:gc 参数后重现启动 Java 进程,输出大致如下:</li>    </ul>    <pre>  <code class="language-java">8190.813: [GC 164675K->251016K(1277056K), 0.0117749 secs] #8190.813 表示垃圾回收的时间点,秒为单位。GC/Full GC 表示垃圾回收的类型  8190.825: [Full GC 251016K->164654K(1277056K), 0.8142190 secs]   # 251016K表示回收前占用的内存大小,164654K 表示回收后占用的内存大小,1277056K 表示当前对内存总大小,0.8142190 表示回收耗时  8191.644: [GC 164678K->251214K(1277248K), 0.0123627 secs]  8191.657: [Full GC 251214K->164661K(1277248K), 0.8135393 secs]  8192.478: [GC 164700K->251285K(1277376K), 0.0130357 secs]  8192.491: [Full GC 251285K->164670K(1277376K), 0.8118171 secs]  8193.311: [GC 164726K->251182K(1277568K), 0.0121369 secs]  8193.323 : [Full GC 251182K->164644K(1277568K), 0.8186925 secs]  8194.156: [GC 164766K->251028K(1277760K), 0.0123415 secs]  8194.169: [Full GC 251028K->164660K(1277760K), 0.8144430 secs]  </code></pre>    <p>怀疑内存泄漏后,我们通过 Full GC 日志进一步确认,检查 Full GC 后的可用内存是否持续增大。步骤如下:</p>    <ul>     <li>获取系统稳定后的 GC 日志(不稳定的日志不可靠)</li>     <li>过滤 FullGC 日志,可能会有如下两种情况      <ul>       <li>FullGC 后内存使用量持续增长,一直到设置的堆内存最大值,基本可以确定内存泄漏</li>       <li>内存使用量增长后又回落,出于一个动态平衡区间,基本排除内存泄漏</li>      </ul> </li>    </ul>    <p>GC 日志只能帮忙找到是否有泄漏,找出内存泄漏的地方,需要依赖一些其他的工具</p>    <ul>     <li>JProfile</li>     <li>OptimizedIt</li>     <li>JProbe</li>     <li>JConsole</li>     <li>-Xrunhprof</li>    </ul>    <h2>3.2 本地内存泄漏的定位</h2>    <p>GC 日志无异常,但 Java 进程使用内存逐渐增大,并且无停止上涨的趋势。本地内存泄漏的原因有如下几个:</p>    <ul>     <li>JNI 调用中出现内存泄漏(JNI 调用出现内存泄漏,可以使用 C/C++ 内存泄漏分析方法定位)</li>     <li>JDK bug</li>     <li>操作系统问题</li>    </ul>    <p>本地内存泄漏可能伴有如下异常</p>    <pre>  <code class="language-java">java.lang.OutOfMemoryError: unable to create new native thread , Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread        at java.lang.Thread.start0(Native Method)        at java.lang.Thread.start(Thread.java:574)        at TestThread.main(TestThread.java:34)  </code></pre>    <p>上面这个异常可能的原因有:</p>    <ul>     <li>创建的线程过多,可打印总线程数查看</li>     <li>swap 分区不足</li>     <li>堆内存过大,本地内存不足</li>    </ul>    <h2>3.3 Perm 区内存不足定位</h2>    <p>出现 java.lang.OutOfMemoryError: PermGen space Perm ,说明 Perm 区内存不足</p>    <ul>     <li>依赖注入,没有卸载</li>     <li>Perm 区太小</li>    </ul>    <p> </p>    <p>来自:http://www.klion26.com/2018/03/14/Java-内存泄漏分析和对内存设置/</p>    <p> </p>