前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM各区溢出分析

JVM各区溢出分析

作者头像
BUG弄潮儿
发布2020-06-15 17:40:46
4730
发布2020-06-15 17:40:46
举报
文章被收录于专栏:JAVA乐园JAVA乐园

阅读文本大概需要3分钟。

0x01:Java虚拟机栈和本地方法栈溢出

由于在Hotspot虚拟机中中不区分虚拟机栈和本地方法栈,因此通过-Xoss修改参数是无效的,可以通过修改-Xss设定。

  • 如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。

这两种异常有一些重叠的部分:当栈空间无法继续分配时,到底是内存太小,还是已经使用的栈空间过大,其本质只是对同一件事情的两种不同描述。

可以通过以下方法验证:

  • 在使用-Xss参数减少栈内存容量,结果抛出Stack OverflowError异常,异常出现时输出的堆栈深度相应缩小。
  • 定义了大量的本地变量,增大此方法栈中本地变量表的长度,结果抛出Stack OverflowError异常时输出的堆栈深度相应缩小。

可以通过递归调用的方式进行测试:

代码语言:javascript
复制
public void stackLeak() {
    stackLeak();
}

通过不断建立线程的方式可以生产内存异常异常,但是产生的内存异常异常和栈空间是否足够大并不存在任何关联,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出。

操作系统为虚拟机分配的内存是有限制的,如果虚拟机进程本身消耗的内存计算在内,剩余的内存就由虚拟机栈和本地方法栈瓜分了,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。

如果是建立线程过多导致内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

可以通过死循环创建线程的方式模拟“由于线程过多导致的内存溢出”:

代码语言:javascript
复制
while(true){
  Thread t = new Thread(new Runable(){
      ......
  });
}

0x02:Java堆内存溢出

可以通过不停的创建对象来造成堆内存溢出

代码语言:javascript
复制
public static void main(String[] args) {
    List list = new ArrayList<>();
    while(true) {
       list.add(new ObjectBIg())
    }
}

使用-XX:+HeapDumpOnOutOfMemoryError可以在虚拟机在出现内存溢出异常时Dump出当前的内存堆转存储快照以便后续进行分析。

对Dump快照进行分析,需要区分出到底是内存泄漏Memory Leak还是内存异常Memory Overflow。

如果是内存泄漏,进一步通过工具对GC Root的引用链进行分析。

如果不是内存泄漏,就是内存中的对象确实都还必须存活,那就应该修改虚拟机参数Xmx Xms,同时判断是否可以通过调大物理内存的方式解决。然后从代码角度检测是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的消耗。

0x03: 方法区和运行时常量池溢出

由于运行时常量池属于方法区的一部分,因此两个区域放在一块执行。

String.intern()是一个Native方法,它的作用是如果字符串常量池中已经包含了此String对象的字符串,则返回代表池中这个字符串的String对象;否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

可以通过以下代码测试运行时常量池溢出:

代码语言:javascript
复制
public class Test {
 public static void main(String[] args) {
      int i =0;
      List<String> list = new ArrayList();
      while(true) {
         list.add(String.valueOf(i++).intern());
      }
   }
}

可以在抛出的异常后面发现“Perm space”信息。

可以使用String.intern()测试运行时常量池:

代码语言:javascript
复制
public class Test1 {
 public static void main(String[] args) {
      String str1 = new StringBuilder("111").append("-222").toString();
      System.out.println(str1.intern()==str1);
 String str2= new 
       StringBuilder("jav").append("a").toString();;
      System.out.println(str2.intern()==str2);
 }
}
结果:
true
false

JDK1.7中的intern实现不会复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。

对str2比较返回false是因为“java”字符串在执行StringBuilder.toString()之前已经出现过了,字符串常量池中已经有它的引用了,不符合“首次出现”的原则。

方法区用于存放Class相关的信息,如类名、访问修饰符、常量池、字段描述、方法描述等,对于这些区域的测试,基本的思路是运行时产生大量的类填充方法区,直到溢出。

可以借助GCLib直接操作字节码运行时产生大量的动态类:

代码语言:javascript
复制
public class Test1 {
     public static void main(final String[] args) {
          while(true){
             Enhancer enhancer = new Enhancer();
             enhancer.setSuperclass(OOMOBject.class);
             enhancer.setUseCache(false);
             enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invoke(objects,args);
              }
            });
            enhancer.create();
       }
   }
     static class OOMOBject{

     }
}

除了GCLib字节码增强和动态语言之外,常见的还有大量JSP或者动态生成JSP文件的应用、基于OSGi的应用等

另外:程序计数器是JVM唯一不会发生内存溢出的区域。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 BUG弄潮儿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档