《深入理解Java虚拟机-JVM高级特性与最佳实践(第三版)》学习日记三

Java内存区域与内存溢出异常

4. OutOfMemoryError异常

  • Java堆溢出

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

    • 代码

      public class HeapOOM {
      
           static class OMMObject{}
      
           public static void main(String[] args) {
               List<OMMObject> list = new ArrayList<OMMObject>();
               while (true) {
                   list.add(new OMMObject());
               }
           }
      }
      
    • 限制Java堆大小

      -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
      
      • -xms:Java堆初始内存
      • -Xmx:Java堆最大可用内存
      • -XX:+HeapDumpOnOutOfMemoryError:在堆内存溢出时保存快照
      堆溢出-jvm设置.png
    • 运行结果

      java.lang.OutOfMemoryError: Java heap space
      Dumping heap to java_pid10796.hprof ...
      Heap dump file created [28832619 bytes in 0.065 secs]</pre>
      
    堆溢出.png
    • 分析

      • Java堆内存溢出时,异常信息“java.lang.OutOfMemoryError”后会跟随进一步提示“Java heap space”
    • 解决

      • 常规的处理方法是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,确认内存中导致OOM的对象是否是必要的,即分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
        • 内存泄漏,通过工具一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置,优化代码
        • 内存溢出,就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗
  • 虚拟机栈和本地方法栈溢出

    《Java虚拟机规范》 中描述了两种异常:

    • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;
      由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,栈深只能由-Xss参数来设定;
    • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常;
      HotSpot虚拟机栈内存不能动态扩展,当线程申请栈空间失败时,就会出现StackOverflowError异常
    • 实验一:

      溢出异常测试思路:
      1、Java虚拟机栈用于存储局部变量表、操作数栈、动态连接、方法出后等信息
      2、使用-Xss参数减少栈内存容量

      • 代码

        public class JavaVMStackSOF {
        
               private int stackLength = 1;
        
               public void stackLeak() {
                   stackLength++;
                   stackLeak();
               }
              
               public static void main(String[] args) {
                   JavaVMStackSOF oom = new JavaVMStackSOF();
                   try {
                       oom.stackLeak();
                   } catch(Throwable e) {
                       System.out.println("stack length:"+oom.stackLength);
                       throw e;
                   }
               }
        }
        
      • 限制栈内存容量

        -Xss128k
        
        • Xss:栈内存大小
        栈溢出-jvm设置.png
      • 运行结果

        stack length:1746
        Exception in thread "main" java.lang.StackOverflowError
               at com.javastudy.operator.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
               ...
        
        栈溢出.png
    • 实验二

      溢出异常测试思路:
      定义大量的局部变量,增大此方法栈帧中局部变量表的长度,直至该栈帧超出栈内存出现溢出异常

      • 代码

        public class JavaVMStackOOM {
        
               private static int stackLength = 0;
        
               public static void test() {
                   long unused1, unused2, unused3, unused4, unused5,
                   unused6, unused7, unused8, unused9, unused10,
                   unused11, unused12, unused13, unused14, unused15,
                   unused16, unused17, unused18, unused19, unused20,
                   unused21, unused22, unused23, unused24, unused25,
                   unused26, unused27, unused28, unused29, unused30,
                   unused31, unused32, unused33, unused34, unused35,
                   unused36, unused37, unused38, unused39, unused40,
                   unused41, unused42, unused43, unused44, unused45,
                   unused46, unused47, unused48, unused49, unused50,
                   unused51, unused52, unused53, unused54, unused55,
                   unused56, unused57, unused58, unused59, unused60,
                   unused61, unused62, unused63, unused64, unused65,
                   unused66, unused67, unused68, unused69, unused70,
                   unused71, unused72, unused73, unused74, unused75,
                   unused76, unused77, unused78, unused79, unused80,
                   unused81, unused82, unused83, unused84, unused85,
                   unused86, unused87, unused88, unused89, unused90,
                   unused91, unused92, unused93, unused94, unused95,
                   unused96, unused97, unused98, unused99, unused100;
                   stackLength ++;
                   test();
                   unused1 = unused2 = unused3 = unused4 = unused5 =
                   unused6 = unused7 = unused8 = unused9 = unused10 =
                   unused11 = unused12 = unused13 = unused14 = unused15 =
                   unused16 = unused17 = unused18 = unused19 = unused20 =
                   unused21 = unused22 = unused23 = unused24 = unused25 =
                   unused26 = unused27 = unused28 = unused29 = unused30 =
                   unused31 = unused32 = unused33 = unused34 = unused35 =
                   unused36 = unused37 = unused38 = unused39 = unused40 =
                   unused41 = unused42 = unused43 = unused44 = unused45 =
                   unused46 = unused47 = unused48 = unused49 = unused50 =
                   unused51 = unused52 = unused53 = unused54 = unused55 =
                   unused56 = unused57 = unused58 = unused59 = unused60 =
                   unused61 = unused62 = unused63 = unused64 = unused65 =
                   unused66 = unused67 = unused68 = unused69 = unused70 =
                   unused71 = unused72 = unused73 = unused74 = unused75 =
                   unused76 = unused77 = unused78 = unused79 = unused80 =
                   unused81 = unused82 = unused83 = unused84 = unused85 =
                   unused86 = unused87 = unused88 = unused89 = unused90 =
                   unused91 = unused92 = unused93 = unused94 = unused95 =
                   unused96 = unused97 = unused98 = unused99 = unused100 = 0;
               }
              
               public static void main(String[] args) {
                   try {
                       test();
                   }catch (Error e) {
                       System.out.println("stack length:"+stackLength);
                       throw e;
                   }
               }
        }
        
      • 运行结果

        stack length:4507
        Exception in thread "main" java.lang.StackOverflowError
               at com.javastudy.operator.JavaVMStackOOM.test(JavaVMStackOOM.java:29)
              ...
        
        栈溢出2.png
      • 分析

        • 结果表明,无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常
        • 可是如果在允许动态扩展栈容量大小的虚拟机上,相同代码则会导致不一样的情况,相同的代码在Classic虚拟机中会产生OutOfMemoryError异常
      • 解决

        • 出现StackOverflowError异常时,会有明确错误堆栈可供分析,相对而言比较容易定位到问题所在
        • 通过不断建立线程的方式,在HotSpot上也是可以产生内存溢出异常,如果是建立过多线程导致的栈内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程
  • 方法区和运行时常量池溢出

    • 运行时常量池

      溢出异常测试思路:
      1、在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量;
      2、 intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则会将此String对象包含的字符串添加到字符串常量池中,并且返回此String对象的引用;
      3、通过产生大量首次的字符串来填满字符串常量池,直至溢出;

      • 代码

        public class RuntimeConstantPoolOOM {
              
               public static void main(String[] args) {
                   // 使用Set保持常量池引用,避免GC回收常量池
                   Set<String> set = new HashSet<String>();
                   //在short范围内足以让6M的PermSize产生OOM
                   short i = 0;
                   while(true) {
                       set.add(String.valueOf(i++).intern());
                   }
               }
         }
        
      • 限制方法区(非堆)的容量

        -XX:PermSize=6M -XX:MaxPermSize=6M
        
        • PermSize:非堆内存初始值
        • MaxPermSize:最大非堆内存
      • 运行结果

        • 在JDK 6或更早之前的HotSpot虚拟机中,运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“PermGen space”,说明运行时常量池(字符串常量池)的确是属于方法区的一部分
        • 但是在JDK 7及以上版本,运行此代码不会溢出异常
      • 分析

        • 因为自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,所以在JDK 7及以上版本,限制方法区的容量对该测试用例来说是毫无意义的,可以通过限制堆内存来产生溢出异常“Java heap space”
    • 方法区

      溢出异常测试思路:
      1、方法区的主要职责是用于存放类型的相关信息, 如类名、访问修饰符、常量池、字段描述、方法描述等;
      2、通过CGLib直接操作字节码,运行时生成了大量的动态类去填满方法区,直到溢出为止;

      • 代码

        import java.lang.reflect.Method;
        //导入cglib.jar包
        import net.sf.cglib.proxy.Enhancer;
        import net.sf.cglib.proxy.MethodInterceptor;
        import net.sf.cglib.proxy.MethodProxy;
              
        public class JavaMethodAreaOOM {
              
               public static void main(String[] args) {
                   // TODO Auto-generated method stub
                   while(true) {
                       Enhancer enhancer = new Enhancer();
                       enhancer.setSuperclass(OOMObject.class);
                       enhancer.setUseCache(false);
                       enhancer.setCallback(new MethodInterceptor() {
                           public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                               return proxy.invokeSuper(obj, args);
                           }
                       });
                       enhancer.create();
                   }
               }
               static class OOMObject{}
        }
        
      • 限制方法区(非堆)的容量

        -XX:PermSize=10M -XX:MaxPermSize=10M
        
        • PermSize:非堆内存初始值
        • MaxPermSize:最大非堆内存
      • 运行结果

        • 在JDK 7中的运行结果,Caused by:java.lang.OutOfMemoryError:PermGen space
        • 但是在JDK 8及以上版本,运行此代码不会溢出异常
      • 分析

        • 因为在JDK 8及以上版本,永久代便完全退出了历史舞台,元空间作为其替代者登场,在默认设置下,前面动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常,可通过限制元空间参数达到目的,产生溢出异常“Metaspace”:

          • -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小
          • -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:
            • 如果释放了大量的空间,就适当降低该值:
            • 如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值;
          • -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率;
          • -XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比;
        • 限制元空间容量

          -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
          
          方法区溢出-jvm设置.png
        • 运行结果

          Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
                   at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)
                   ...
          
          方法区溢出.png
  • 本机直接内存溢出

    溢出异常测试思路:
    1、直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致;
    2、通过反射获取Unsafe实例的allocateMemory() 进行内存分配,当内存无法分配抛出溢出异常

    • 代码

      import java.lang.reflect.Field;
      import sun.misc.Unsafe;
          
      public class DirectMemoryOOM {
      
           private static final int _1MB = 1024 * 1024;
          
           public static void main(String[] args) throws Exception {
               // TODO Auto-generated method stub
               Field unsafeField = Unsafe.class.getDeclaredFields()[0];
               unsafeField.setAccessible(true);
               Unsafe unsafe = (Unsafe) unsafeField.get(null);
               while (true) {
                   unsafe.allocateMemory(_1MB);
               }
           }
      }
      
    • 限制直接内存容量

      -Xmx20M -XX:MaxDirectMemorySize=10M
      
      • MaxDirectMemorySize:直接内存(Direct Memory) 的容量大小
      • -Xmx:Java堆最大可用内存
      直接内存溢出-jvm设置.png
    • 运行结果

      Exception in thread "main" java.lang.OutOfMemoryError
           at sun.misc.Unsafe.allocateMemory(Native Method)
           at com.javastudy.operator.DirectMemoryOOM.main(DirectMemoryOOM.java:17)    
      
      直接内存溢出.png
    • 分析

      • 直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容