JVM各区溢出分析

2020-06-15 17:40:46 浏览数 (1)

阅读文本大概需要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唯一不会发生内存溢出的区域。

0 人点赞