• JVM-各内存区域抛出OutOfMemoryError异常分析


    OutOfMemoryError异常

    • 注:本人测试基于jdk1.8测试,有部分不同但是原理可以了解,感兴趣可以下载jdk1.7配套测试
    • Java虚拟机规范中描述,除了程序计数器,虚拟机的其他几个运行时区域都有发生OOM异常的可能。 下面的示例代码都基于HotSpot虚拟机运行,设置VM参数可以在IDE的VM options内设置,如图 

    Java堆溢出

    引发思路:Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

    • 以下代码需要配置VM,设置java堆大小20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析 -Xmx:最大堆大小
    -Xmx20M -Xms20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:jvmlogoom.hprof
    

    可以指定hprof文件存放位置,之后使用jdk自带工具jvisualvm.exe打开分析即可

    import java.util.ArrayList;
    import java.util.List;
    
    public class HeapOOM {
        static class OOMObject {
        }
        public static void main(
                String[] args) {
            List<OOMObject> list = new ArrayList<OOMObject>();
            while (true) {
                list.add(new OOMObject());
            }
        }
    }
    

    运行结果:

    java.lang.OutOfMemoryError: Java heap space
    Dumping heap to java_pid12092.hprof ...
    Heap dump file created [28256955 bytes in 0.096 secs]
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    	at java.util.Arrays.copyOf(Arrays.java:3210)
    	at java.util.Arrays.copyOf(Arrays.java:3181)
    	at java.util.ArrayList.grow(ArrayList.java:267)
    	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
    	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
    	at java.util.ArrayList.add(ArrayList.java:464)
    	at com.llh.jdk.map.HeapOOM.main(HeapOOM.java:14)
    

    Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。

    • 如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。
    • 如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

    虚拟机栈和本地方法栈溢出

    1. 在java虚拟机栈中描述了两种异常:
      • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
      • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
    2. 定义大量的本地变量,增大此方法栈中本地变量表的长度,设置-Xss参数减少栈内存容量
      -Xss20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:jvmlogsof.hprof
      
      public class JavaVMStackSOF {
          private int stackLength = 1;
      
          public void stackLeak() {
              stackLength++;
              stackLeak();
          }
          public static void main(String[] args) throws Throwable {
              JavaVMStackSOF oom = new JavaVMStackSOF();
              try {
                  oom.stackLeak();
              } catch (Throwable e) {
                  System.out.println("stack length:" + oom.stackLength);
                  throw e;
              }
          }
      }
      
      运行结果
      stack length:1271382
      Exception in thread "main" java.lang.StackOverflowError
          at com.llh.jdk.map.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
          at com.llh.jdk.map.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
          at com.llh.jdk.map.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
          at com.llh.jdk.map.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
          at com.llh.jdk.map.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
          at com.llh.jdk.map.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
          at com.llh.jdk.map.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
          at com.llh.jdk.map.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
      ···
      
      实验结果: 单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。

    3.如果不限于单线程,通过不断建立线程的方式可以产生内存溢出异常,这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

    • 原因:操作系统分配给每个进程的内存是有限制的,虚拟机提供参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为操作系统限制减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量)。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存由虚拟机栈和本地方法栈瓜分。每个线程分配到的栈容量越大,可以建立的线程数量越少,建立线程时越容易把剩下的内存耗尽
    • 解决:如果是建立多线程导致内存溢出,在不能减少线程数或者更换虚拟机的情况下,通过减少堆的最大堆和减少栈容量来换取更多的线程。

    创建线程导致内存溢出

    -Xss20M
    
    public class JavaVMStackOOM {
        private void dontStop(){
            while(true){
            }
        }
        public void stackLeakByThread(){
            while(true){
                Thread thread=new Thread(this::dontStop);
                thread.start();
            }
        }
        public static void main(String[]args)throws Throwable{
            JavaVMStackOOM oom=new JavaVMStackOOM();
            oom.stackLeakByThread();
        }
    }
    

    运行结果

    Exception in thread"main"java.lang.OutOfMemoryError:unable to create new native thread
    

    注意:在windows上,Java线程是映射到操作系统的内核线程上的,执行此代码会导致操作系统假死。

    方法区和运行时常量池内存溢出

    • String.intern()是一个Native方法

      • 作用:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象; 否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
    • 运行时常量池导致的内存溢出(因为笔者使用的jdk1.8,所以设置元空间来测试常量池内存溢出情况)

      -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
      
      import java.util.ArrayList;
      import java.util.List;
      public class RuntimeConstantPoolOOM {
          public static void main(String[] args) {
      //使用List保持着常量池引用,避免Full GC回收常量池行为
              List<String> list = new ArrayList<String>();
      //10MB的PermSize在integer范围内足够产生OOM了
              int i = 0;
              while (true) {
                  list.add(String.valueOf(i++).intern());
              }
          }
      }
      

      在jdk1.7下会一直运行下去,在看一段代码测试String.intern()方法

      import java.util.ArrayList;
      import java.util.List;
      public class RuntimeConstantPoolOOM {
         public static void main(String[] args) {
              String str1 = new StringBuilder("计算机").append("软件").toString();
              System.out.println(str1.intern() == str1);
              String str2 = new StringBuilder("ja").append("va").toString();
              System.out.println(str2.intern() == str2);
          }
      }
      

      在1.7与1.8版本的jdk中,这个代码执行会得到一个true,一个false,jdk1.7的intern()方法会在常量池中记录首先出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

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

    • 笔者采用CGLIB直接操作字节码运行时产生大量的动态类。很多主流框架,如Spring、Hibernate,在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。

    -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M -XX:+PrintGCDetails
    
    import org.springframework.cglib.proxy.Enhancer;
    import org.springframework.cglib.proxy.MethodInterceptor;
    public class JavaMethodAreaOOM {
        public static void main(
                String[] args) {
            while (true) {
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
                enhancer.create();
            }
        }
        static class OOMObject {
        }
    }
    

    运行结果

    Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace
    	at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:538)
    	at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
    	at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585)
    	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)
    	at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
    	at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572)
    	at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:387)
    	at com.llh.jdk.map.JavaMethodAreaOOM.main(JavaMethodAreaOOM.java:14)
    
    • 方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了CGLib字节码增强和动态语言之外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

    本地直接内存溢出

    DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样

    • 使用unsafe分配本机内存
    -XX:MaxDirectMemorySize=50M -XX:+PrintGCDetails
    
    import sun.misc.Unsafe;
    import java.lang.reflect.Field;
    public class DirectMemoryOOM {
        private static final int _1MB = 1024 * 1024;
        public static void main(String[] args) throws Exception {
            Field unsafeField = Unsafe.class.getDeclaredFields()[0];
            unsafeField.setAccessible(true);
            Unsafe unsafe = (Unsafe) unsafeField.get(null);
            while (true) {
                unsafe.allocateMemory(_1MB);
            }
        }
    }
    

    运行结果

    Exception in thread "main" java.lang.OutOfMemoryError
    	at sun.misc.Unsafe.allocateMemory(Native Method)
    	at com.llh.jdk.map.DirectMemoryOOM.main(DirectMemoryOOM.java:15)
    Heap
     PSYoungGen      total 75264K, used 5161K [0x000000076ca00000, 0x0000000771e00000, 0x00000007c0000000)
      eden space 64512K, 8% used [0x000000076ca00000,0x000000076cf0a638,0x0000000770900000)
      from space 10752K, 0% used [0x0000000771380000,0x0000000771380000,0x0000000771e00000)
      to   space 10752K, 0% used [0x0000000770900000,0x0000000770900000,0x0000000771380000)
     ParOldGen       total 172032K, used 0K [0x00000006c5e00000, 0x00000006d0600000, 0x000000076ca00000)
      object space 172032K, 0% used [0x00000006c5e00000,0x00000006c5e00000,0x00000006d0600000)
     Metaspace       used 3348K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 366K, capacity 388K, committed 512K, reserved 1048576K
    

    由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。

  • 相关阅读:
    CoInitialize和CoInitializeEx
    ras api win7 和 win xp 遍历时的不同
    jquery实现多行文字图片滚动效果
    js点击button按钮跳转到页面代码
    phpmailer使用163邮件发送邮件例子
    linux中mail函数不能发送邮件
    FreeBSD修改root密码错误passwd: pam_chau(www.111cn.net)thtok(): error in service module from:http://www.111cn.net/sys/freebsd/66713.htm
    asp.net中c# TextBox.MaxLength例子
    WampServer修改Mysql密码的步骤
    dedecms5.7文章实现阅读全文功能二次开发
  • 原文地址:https://www.cnblogs.com/javallh/p/14208083.html
Copyright © 2020-2023  润新知