OOM(OutOfMemoryError异常实战)

OOM(OutOfMemoryError异常实战),第1张

OOM(OutOfMemoryError异常实战) 堆溢出

Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
在运行期我设置了-Xms20m -Xmx20m 使得堆内存不可扩展,否则等待时间比较长。

public class Stack666777 {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List list = new ArrayList();
        while (true) {
            list.add(new OOMObject());
        }
    }

}


-XX:+HeapDumpOnOutOfMemoryError生成堆内存映像,可以使用可视化工具打开进行分析,判断是内存泄漏还是内存溢出。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

虚拟机栈或者本地方法栈溢出
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;
}
}
}

栈深度达到最大值后只会报StackOverflowError(不允许动态扩展的情况)
注意:hotspot不支持栈的动态扩展,因此只会报StackOverflowError
而栈oom错误的发生场景是给线程分配栈内存,这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于 *** 作系统本身的内存使用状态。
32位Windows的单个进程最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,那剩余的内存即为2GB( *** 作系统限制)减去最大堆容量,再减去最大方法区容量,由于程序计数器消耗内存很小,可以忽略掉,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存就由虚拟机栈和本地方法栈来分配了。因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。

栗子:使用-Xss来设置栈初始内存,设置的稍大一点(比如1m)这样可创建线程数量减少。

public class Stack666777 {
    private void donStop(){
        while (true){
            
        }
    }
    public void stackLead(){
        while (true){
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    donStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        Stack666777 a=new Stack666777();
        a.stackLead();
    }

}

尽量别尝试运行,因为设计到 *** 作系统的内存,电脑会重启的,,,

处理与解决:不同于StackOverflowError明确的错误提示,此类错误不易找到原因(不易联想到线程创建问题),如果所需线程数量不可减少,就只能通过参数限制最大堆容量和栈容量(同等内存下,栈容量越小,可以建立的线程数量越多)

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

String:intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的
字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加
到常量池中,并且返回此String对象的引用。
jdk6设置永久代大小就可以导致oom,到了jdk7设置永久代大小就不会导致了,因为已经将字符串常量池移到堆中,这时应当限制堆大小进行测试。
提示下面信息则表示常量池的错误

// OOM异常一:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.lang.Integer.toString(Integer.java:440)
at java.base/java.lang.String.valueOf(String.java:3058)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)

String.intern()返回引用的测试

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);
	}
}

这段代码在jdk6中会返回两个false,通过intern方法将字符串放入永久代,返回的引用是永久代的字符串,自然和原本存储在堆中的字符串地址不同。
在jdk7中返回一个true和一个false,此时intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,那么返回的引用和堆中的引用一致,自然是true;java字符串已经在常量池中,intern返回的是第一次放入常量池的字符串地址,和此时的字符串地址不同。

实战演练方法区溢出(通过cglib动态加载多个类达到溢出目的)

 public static void main(String[] args) {
        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 {
    }

在jdk7产生的报错

Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
... 8 more

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

这类场景除了之前提到的程序使用了CGLib字节码增强和动态语言外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

由直接内存导致的内存溢出

可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。
溢出时一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5708688.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存