jvm内存溢出分析

ekec1153 7年前
   <h2><strong>概述</strong></h2>    <p>jvm中除了程序计数器,其他的区域都有可能会发生内存溢出</p>    <h2><strong>内存溢出是什么?</strong></h2>    <p>当程序需要申请内存的时候,由于没有足够的内存,此时就会抛出OutOfMemoryError,这就是内存溢出</p>    <h2><strong>内存溢出和内存泄漏有什么区别?</strong></h2>    <p>内存泄漏是由于使用不当,把一部分内存“丢掉了”,导致这部分内存不可用。</p>    <p>当在堆中创建了对象,后来没有使用这个对象了,又没有把整个对象的相关引用设为 <em>null</em> 。此时垃圾收集器会认为这个对象是 <em>需要</em> 的,就不会清理这部分内存。这就会导致这部分内存不可用。</p>    <p>所以 <strong>内存泄漏</strong> 会导致可用的内存减少,进而会导致 <strong>内存溢出</strong> 。</p>    <h2><strong>用到的jvm参数</strong></h2>    <p>下面为了说明溢出的情景,会执行一些实例代码,同时需要给jvm指定参数</p>    <ul>     <li>-Xms 堆最小容量(heap min size)</li>     <li>-Xmx 堆最大容量(heap max size)</li>     <li>-Xss 栈容量(stack size)</li>     <li>-XX:PermSize=size 永生代最小容量</li>    </ul>    <p>-XX:MaxPermSize=size 永生代最大容量</p>    <h2><strong>堆溢出</strong></h2>    <p>堆是存放对象的地方,那么只要在堆中疯狂的创建对象,那么堆就会发生内存溢出。</p>    <p>下面做一个堆溢出的实验</p>    <p>执行这段代码的时候,要给jvm指定参数</p>    <pre>  <code class="language-java">//jvm参数:-Xms20m -Xmx20m  public class HeapOOMTest {      public static void main(String[] args){          LinkedList<HeapOOMTest> l=new LinkedList<HeapOOMTest>();//作为GC Root          while(true){              l.add(new HeapOOMTest());//疯狂创建对象          }      }  }</code></pre>    <p><em>-Xms20m -Xmx20m</em> 作用是将jvm的最小堆容量和最大堆容量都设定为20m,这样就 <strong>不会动态扩展jvm堆</strong> 了</p>    <p>这段代码疯狂的创建对象,虽然对象没有声明变量名引用,但是将对象添加到队列l中,这样 <strong>队列l就持有了一份对象的引用</strong></p>    <p>通过可达性算法(jvm判断对象是否可被收集的算法)分析,队列l作为GC Root, <strong>每一个对象都是l的一个可达的节点</strong> ,所以疯狂创建的对象 <strong>不会被收集</strong> ,这就是内存泄漏,这样总有一天堆就溢出了。</p>    <p>运行结果:</p>    <pre>  <code class="language-java">Exception in thread "main" java.lang.OutOfMemoryError: Java heap space      at java.util.LinkedList.linkLast(Unknown Source)      at java.util.LinkedList.add(Unknown Source)      at test.HeapOOMTest.main(HeapOOMTest.java:23)</code></pre>    <p>程序发生内存溢出,并提示发生在 <em>Java heap space</em></p>    <h2><strong>分析解决方法</strong></h2>    <h3><strong>思路</strong></h3>    <p>用 visualVM 工具分析堆快照</p>    <p>如果发生 <strong>内存泄漏</strong> :</p>    <p>step1:找出泄漏的对象</p>    <p>step2:找到泄漏对象的GC Root</p>    <p>step3:根据泄漏对象和GC Root找到导致内存泄漏的代码</p>    <p>step4:想法设法解除泄漏对象与GCRoot的连接</p>    <p>如果 <strong>不存在泄漏</strong> :</p>    <ol>     <li>看下是否能增大jvm堆的最大容量</li>    </ol>    <p>优化程序,减小对象的生命周期</p>    <h3><strong>前期准备</strong></h3>    <p>当发生堆溢出的时候,可以让程序在崩溃时产生一份 <strong>堆内存快照</strong></p>    <p>产生堆内存快照的方法:</p>    <p>给jvm加上参数 <strong>XX:+HeapDumpOnOutofMemoryError</strong> ,这样就会在程序崩溃的时候,产生一份堆内存快照</p>    <p>分析堆内存快照我建议用jdk自带的可视化监视工具visualVM,位置在jdk安装目录下的bin,如果是在linux环境的话,可以把快照传到window。因为分析工具会占用很大的内存,不建议在服务端进行分析。</p>    <h3><strong>实战</strong></h3>    <p>下面对刚才程序产生的堆内存快照进行分析。</p>    <p>打开visualVM,装入刚刚生成的快照,打开类标签页</p>    <p><img src="https://simg.open-open.com/show/1cff1b70b2237d64cb10e6837fd4b139.png"></p>    <p>队列和疯狂创建的对象几乎占满了整个栈,想要让垃圾收集器回收这些对象,要让他们 <strong>与GC Root断开连接</strong></p>    <p>双击HeapOOMTest类,跳转到实例标签页,可以查看这个类的所有实例</p>    <p>在实例上右键——显示最近的垃圾回收根节点,可以 <strong>看到这个对象与根节点的连接</strong></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f396de1c61964552034ba015d93b08e8.png"></p>    <p>只要断开HeapOOMTest对象与LinkedList的连接,这些疯狂创建的对象就会被收集了</p>    <h2><strong>栈溢出</strong></h2>    <p>调用方法的时候,会在栈中入栈一个栈帧,如果当前栈的容量不足,就会发生栈溢出 <strong>StackOverFlowError</strong></p>    <p>那么只要疯狂的调用方法,并且有意的不让栈帧出栈就可以导致栈溢出了。</p>    <p>下面来一次栈溢出</p>    <pre>  <code class="language-java">//jvm参数:-Xss128k  public class StackSOFTest {      public void stackLeak(){          stackLeak();//递归,疯狂的入栈,有意不让出栈      }       public static void main(String[] args){          StackSOFTest s=new StackSOFTest();          s.stackLeak();      }  }</code></pre>    <p>jvm设置参数 <em>-Xss128k</em> ,目的是缩小栈的空间,这样栈溢出“来的快一点”</p>    <p>程序中用了递归,让栈帧疯狂的入栈,又不让栈帧出栈,这样就会栈溢出了。</p>    <p>运行结果:</p>    <pre>  <code class="language-java">Exception in thread "main" java.lang.StackOverflowError      at test.StackSOFTest.stackLeak(StackSOFTest.java:17)      at test.StackSOFTest.stackLeak(StackSOFTest.java:17)</code></pre>    <h2><strong>运行时常量池溢出</strong></h2>    <p>这里储存的是一些常量、字面量。如果运行时常量池内存不足,就会发生内存溢出。从jdk1.7开始,运行时常量池移动到了堆中,所以如果堆的内存不足,也会导致运行时常量池内存溢出。</p>    <p>下面来一次运行时常量池溢出,环境是jdk8</p>    <p>只要创建足够多的常量,就会发生溢出</p>    <pre>  <code class="language-java">/**   * jvm参数:   * jdk6以前:-XX:PermSize=10M -XX:MaxPermSize=10M   * jdk7开始:-Xms10m -Xmx10m   * */  public class RuntimePoolOOM {      public static void main(String[] args){          int i=1;          LinkedList<String> l=new LinkedList<String>();//保持常量的引用,防止被fullgc收集          while(true){              l.add(String.valueOf(i++).intern());//将常量添加到常量池          }      }  }</code></pre>    <p>因为jdk6以前,运行时常量池是在方法区(永生代)中的,所以要限制永生代的容量,让内存溢出来的更快。</p>    <p>从jdk7开始,运行时常量池是在堆中的,那么固定堆的容量就好了</p>    <p>这里用了链表去保存常量的引用,是因为防止被fullgc清理,因为fullgc会清理掉方法区和老年代</p>    <p>intern()方法是将常量添加到常量池中去,这样运行时常量池一直都在增长,然后内存溢出</p>    <p>运行结果:</p>    <pre>  <code class="language-java">Exception in thread "main" java.lang.OutOfMemoryError: Java heap space      at java.lang.Integer.toString(Unknown Source)      at java.lang.String.valueOf(Unknown Source)      at test.RuntimePoolOOM.main(RuntimePoolOOM.java:30)</code></pre>    <p>提示在heap区域发生内存溢出,果然运行时常量池被移到了堆中</p>    <h2><strong>方法区溢出</strong></h2>    <p>方法区是存放类的信息,而且很难被gc,只要加载了大量类,就有可能引起方法区溢出</p>    <p>这里将不做演示了,想试试的可以用cglib创建大量的代理类</p>    <h2><strong>分析</strong></h2>    <p>工作中也有可能会遇上方法区溢出:</p>    <p>当多个项目都有 <strong>相同jar包</strong> 的时候,又都存放在WEB-INF\lib\下,这样每个项目都会加载一遍jar包。会导致方法区中有 <strong>大量相同类</strong> (被不同的类加载器所加载),又不会被gc掉。</p>    <h3><strong>解决方案:</strong></h3>    <ol>     <li> <p>在应用服务器中建立一个 <strong>共享lib库</strong> ,把项目中常用重复的jar包存放在这里,项目从这里加载jar包,这样就会大大减少类加载的数量,方法区也“瘦身”了</p> </li>     <li> <p>如果实在不能瘦身类的话,那可以扩大方法区的容量,给jvm指定参数 <strong>-XX:MaxPermSize=xxxM</strong></p> <p> </p> </li>    </ol>    <p>来自:http://www.cnblogs.com/wewill/p/6038528.html</p>    <p> </p>