【深入理解JVM-OutOfMemoryError实战】

除了程序计数器外jvm的其他运行时数据区域都有可能出现OOM本节就简单的实战下,以及总结下idea下踩得坑。

一、前言

1、本节目的
  • 通过代码验证jvm各个运行时数据区域存储的内容
  • 实际工作中遇到OOM时,能根据异常信息快速判断哪个区域的OOM,知道什么样的代码可能导致这些区域的OOM,以及出现OOM后如何处理。
2、idea上设置jvm参数采坑

不要把idea的jvm启动参数当成设置jvm参数

(1)错误设置方式1

1、打开idea安装目录的bin目录
2、记事本打开idea64.exe.vmoptions文件
3、-Xms20m-Xmx20m
4、保存重启idea
5、idea炸了,打不开。(Internal error. Please report to http://jb.gg/ide/critical-startup-errors)

(2)错误设置方式2

1、idea - help -Edit custom Optionms - 弹窗(如下)- create
2、创建了文件
3、修改参数-Xms20m-Xmx20m
4、保存重启又炸了

在这里插入图片描述

(3)idea启动时报上述bug解决

1、删除文件:
c盘-user-Administrator-.IntelliJIdea20xx.x文件夹
2、重启idea安装下。

3、正确的设置jvm参数

(1)点击Edit Config…
在这里插入图片描述
(2)找到编辑窗口点击打开
在这里插入图片描述
(3)参数配置(我的如下)
在这里插入图片描述

(4)返回此窗口找到Save XXX(我的为Save OOMHeap)类保存下

在这里插入图片描述

(5)常用参数

------------------------------------------------------------------------
1、堆:
-Xms:JVM初始分配的堆内存
-Xmx:JVM最大允许分配的堆内存,按需分配
------------------------------------------------------------------------
2、栈:
-Xss 为jvm启动的每个线程分配的栈内存大小,默认JDK1.4中是256K,JDK1.5+中是1M 
------------------------------------------------------------------------
3、方法区:
-XX:PermSize   初始内存
-XX:MaxPermSize 最大内存
------------------------------------------------------------------------
4、本机直接内存
-XX:MaxDirectByteBuffer
------------------------------------------------------------------------
5、生成堆的log文件:
-XX:+HeapDumpOnOutOfMemoryError:让虚拟机在堆内存溢出时生成快照日志
-XX:HeapDumpPath=C:\Users\Administrator\Desktop\error_logs:生成快照日志的路径(这里为桌面)
------------------------------------------------------------------------

二、OOM实战

1、java堆溢出(heap oom)

(1)首先设置参数

-Xms60m 
-Xmx60m 
-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=C:\Users\Administrator\Desktop\error_logs

(2)代码模拟及其结果

/**
 * Create by SunnyDay on 2019/10/05
 */
public class OOMHeap {
    
    
    static class OOMObject{
    
    }
    
    public static void main(String[] args){
    
    
        List<OOMObject> list = new ArrayList<>();
        while (true){
    
    
            list.add(new OOMObject());
        }
    }
}
// log:
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:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at test.OOMHeap.main(OOMHeap.java:15)

如上出现堆内存溢出时会提示java.lang.OutOfMemoryError: Java heap space

(3)工具分析
在这里插入图片描述
在这里插入图片描述

使用这个工具可以分析生成的error_log文件
分析是内存泄漏还是oom
1、内存泄漏:通过工具查看GC root引用链,便可找到泄漏对象是通过怎样路径与GC root相关联的,导致垃圾回收器无法回收。(也就是大对象一直是被引用而无法回收这既是内存泄漏,泄漏多了就导致oom)
2、不为内存泄漏:这说明内存中的对象却是需要存活。

  • 这时应当检测虚拟机的堆参数(-Xms与、-Xmx)与物理内存对比看是否可以调大
  • 代码上检测是否存在某些对象生命周期过长、持有状态时间过长,来尝试减少代码运行期的内存消耗。

ps:以上只是解决的简单思路,工具的使用与验证参考后面第三章的笔记。

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

由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈。因此对HotSpot虚拟机来说-Xoss参数(设置本地方法栈大小)无效。栈容量由-Xss设置。

(1)这两个区域的异常回顾

1、如果线程请求的深度大于jvm所允许的最大深度,将抛出Stack Overflow
2、如果虚拟机扩展栈时无法申请到足够的内存空间将抛出OOM。
疑问:
当栈空间无法分配时,到底是内存太小,还是已经使用的栈空间太大。
解释:上述的状况本质上是针对同一件事情的不同描述而已。

(2)实验测试:单线程下设置内存为128K


package test;
/**
 * Create by SunnyDay on 2019/10/05
 * -Xss128k
 */
public class StackSOF {
    
    

    private int stackLength = 1;

    public void stackLeak() {
    
    
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
    
    
        StackSOF stackSOF = new StackSOF();
        try {
    
    
            stackSOF.stackLeak();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

// log:
Exception in thread "main" java.lang.StackOverflowError
	at test.StackSOF.stackLeak(StackSOF.java:8)
	at test.StackSOF.stackLeak(StackSOF.java:9)
	at test.StackSOF.stackLeak(StackSOF.java:9)
	...
	...
	at test.StackSOF.stackLeak(StackSOF.java:9)
	at test.StackSOF.main(StackSOF.java:15)

如上限制条件:
1、使用-Xss减小栈容量,结果抛出Stack Overflow,异常时输出的栈深度相应缩小。
2、定义大量本地变量,增大此方法中本地变量表的长度。结果抛出Stack Overflow,异常时输出的栈深度相应缩小。

(3)单线程下结论

在单线程下,无论是栈帧太大还是虚拟机栈容量太小,当内存无法分配时jvm抛出的都是Stack Overflow异常

(4)多线程下结论

通过不断的创建线程的方式倒是可以产生oom,但是这样产生的内存溢出与栈空间是否足够大无关。,这种情况下为每个线程栈分配的内存越大,反而越容易产生oom。

原因:

1、操作系统分配给每个进程的内存是有限先。例如32位的windows限制单个进程使用内存上线为2GB
2、本地方法栈内存+java栈内存=2GB-堆最大值-MaxPermSize(最大方法区容量)
ps:程序计数器的内存很小忽略、jvm虚拟机进程本身消耗内存不计算在内时。每个线程分配的栈容量越大,可以建立的线程数量自然就越小,建立线程时就越容易吧剩余的内存耗尽。

(5)解决方案

单线程内存溢出:
1、出现Stack Overflow异常时有错误的堆栈可以阅读。方便找出问题所在。
2、如果使用了虚拟机默认参数,栈深度在大多情况下达到1000-2000是完全没问题的,对于正常方法调用包括递归都是完全够用的。
多线程导致的内存溢出:
1、在不能减少线程数或者不更换64位虚拟机情况下,只能通过减少最大堆减少栈容量换取更多线程。

(6)代码

package test;
/**
 * Create by SunnyDay on 2019/10/06
 */
public class StackOOM {
    
    
    
    private void  dontStop(){
    
    
        while(true){
    
    
            
        }
    }
    
    public void moreThread(){
    
    
        while(true){
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                   dontStop(); 
                }
            }).start();
        }
    }
    public static void main(String[] args) {
    
    
        new StackOOM().moreThread();
    }
}

注意:执行上述代码需要保存当前工作。
在windows平台中java的线程是映射到操作系统的内核线程上的。 上述可能导致操作系统假死。

3、方法区和运行时常量池溢出

(1)调整方法区空间大小

-XX:PermSize=500k
-XX:MaxPermSize=500k

(2)code1

/**
 * Create by SunnyDay on 2019/10/06
 */
public class ConstantPoolOOM {
    
    
    public static void main(String[] args) {
    
    
      
        List<String> list = new ArrayList<>();
        int i = 0;
        while(true){
    
    
            // 一直往常量池放不同对象
            list.add(String.valueOf(i++).intern());
            System.out.println();
        }
    }
}

1、上述代码在jdk1.6或者版本小于1.6版本出现OOM
2、jdk1.7或者以后版本程序一直运行(1.7开始常量池移出方法区(永久代))

(2)再次回顾String的intern方法

1、是一个native方法
2、如果字符串常量池中已经有一个等于(equals)此String对象的字符串,则返回常量池中此字符串的对象。否则将String对象包含的字符串添加到常量池,并返回string对象的引用。

       String string1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(string1.intern() == string1);
    

1、jdk1.6中为false(两对象引用不同,一个是堆上的地址,一个是添加到常量池,又为之创建个地址)。jdk1.8中 true(new时是堆上创建内存地址,intern是在常量池记录下这个地址为同一个对象地址)
2、jdk1.7以后intern方法不再复制实例,只是在常量池记录下首次出现的实例引用
3、延伸其实java默认的字符串已经出现在常量池中如(com.sun.glass.ui.Application)Application类中的DEFAULT_NAME。

(3)实验

方法区存放运行时的class信息。基本思路运行时产生大量类填满方法区。

方法区溢出也是常见的OOM,在经常动态生成大量的class的应用中需要特别注意类的垃圾回收状况。
常见的可以使方法区溢出场景:
1、CGLib技术使用
2、大量JSP动态产生jsp文件(jsp第一次运行时编译为clss文件)
3、基于OSGi的应用(同一个类被不同的类加载器加载时视为不同的类)

4、本机直接内存溢出

不手动指定时默认与最大堆的默认值一致。

(1)栗子

//-XX:MaxDirectByteBuffer10M     限制为10M

 Field  field = Unsafe.class.getDeclaredFields()[0] ;
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        while (true){
    
    
            unsafe.allocateMemory(1024*1024);
        }
// logs:
Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at methodarea.MethodAreaOOM.main(MethodAreaOOM.java:22)

由直接内存导致的OOM,明显的特征:
1、Heap Dump导出的日志文件中看不到明显的异常
2、error日志文件小,程序简介或直接使用NIO可以考虑下是否是这个原因

猜你喜欢

转载自blog.csdn.net/qq_38350635/article/details/102164205
今日推荐