关于JAVA的常见内存溢出问题

一、JVM内存溢出的情况

  • 程序计数器(Program Counter Register)
    每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。该内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(内存溢出:OutOfMemoryError)情况的区域。
  • Java虚拟机栈(Java Virtual Machine Stacks)
    在Java虚拟机规范中,对这个区域规定了两种异常情况:
    1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
    2.如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
    这两种情况存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
    在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是StackOverflowError异常,而不会得到OutOfMemoryError异常。
    而在多线程环境下,则会抛出OutOfMemoryError异常。
  • 堆Java Heap
    Java Heap是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
    根据Java虚拟机规范的规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。
  • 方法区域,又被称为“永久代”,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

二、JAVA内存溢出的几种情况

1.Java堆溢出

  • 模拟场景
    Java堆用于存储对象,只要不断的创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,
    那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * 将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展。
 * @author tsh
 */
public class HeapOOM {
 
    static class OOMObject {
    }
 
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
 
        while (true) {
            list.add(new OOMObject());
        }
    }
}
/*
result:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid9220.hprof ...
Heap dump file created [27717826 bytes in 0.160 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
   at java.util.Arrays.copyOf(Arrays.java:2245)
   at java.util.Arrays.copyOf(Arrays.java:2219)
   at java.util.ArrayList.grow(ArrayList.java:242)
   at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
   at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
   at java.util.ArrayList.add(ArrayList.java:440)
   at com.lindaxuan.outofmemory.HeapOOM.main(HeapOOM.java:19)
   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
   at java.lang.reflect.Method.invoke(Method.java:606)
   at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
 */

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

public class StackOutOfMemoryError {
    
    public static void main(String[] args) {
        
         int i = 0;
         go(i);
    }
    private static void go(int i) {
        System.out.println(i++);
        String[] s = new String[100];
        go(i);
    }
}

HotSpot虚拟机中不区分虚拟机栈和本地方法栈。栈容量用-Xss参数设定。Java虚拟机规范中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机锁允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
  1. 1.StackOverflowError异常
/**
 * VM Args:-Xss128k
 * Error: Could not create the Java Virtual Machine.
 Error: A fatal exception has occurred. Program will exit.
 The stack size specified is too small, Specify at least 160k
 VM Args:-Xss256k
 * @author tsh
 */
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;
        }
    }
}
 
/*
result:
stack length:1868
Exception in thread "main" java.lang.StackOverflowError
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:18)
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
   ...
 */
  • 2.OutOfMemoryError异常
/**
 * VM Args:-Xss2M (这时候不妨设大些)
 * @author tsh
 */
public class JavaVMStackOOM {
 
    private void dontStop() {
        while (true) {
        }
    }
 
    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
public static void main(String[] args) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}
/*
my result:
run too long
 */

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

  • 运行时常量区溢出
    下面这段代码需要jdk1.6模拟。
/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 * @author tsh
 * could not download jdk1.6 for macos
 */
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());
        }
    }
}
/*
result:
run too long
 */

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

public class RuntimeConstantPoolOOM2 { 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);
    }
} /* result:
true
false */

对于jdk1.6,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用。
而StringBuilder创建的字符串实例在Java堆,所以必然不是同一个引用,将返回false。
而jdk1.7中的intern()实现不会复制实例,只是在常量池中首次出现的实例引用,因此intern()返回的引用和由StringBuild创建的那个字符串实例是同一个。

  • 运行时方法区溢出
    下面一段代码借助CGLib使方法区出现内存溢出异常。
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
 
import java.lang.reflect.Method;
 
/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author tsh
 */
public class JavaMethodAreaOOM {
 
    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 {
 
    }
}
/*
 * result:
 Exception in thread "main"
 Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
*/

4.本机直接内存溢出

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

import sun.misc.Unsafe;
 
import java.lang.reflect.Field;
 
/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 * @author tsh
 */
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);
        }
    }
}

三、Java代码导致OutOfMemoryError错误的解决:

  • 检查代码中是否有死循环或递归调用。
  • 检查是否有大循环重复产生新对象实体。
  • 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
  • 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

四、解决java.lang.OutOfMemoryError的方法有如下几种:

  • 增加jvm的内存大小。方法有:
    1)在执行某个class文件时候,可以使用java -Xmx256M aa.class来设置运行aa.class时jvm所允许占用的最大内存为256M。
    2)对tomcat容器,可以在启动时对jvm设置内存限度。对tomcat,可以在catalina.bat中添加:

set CATALINA_OPTS=-Xms128M -Xmx256M
set JAVA_OPTS=-Xms128M -Xmx256M
或者把%CATALINA_OPTS%和%JAVA_OPTS%代替为-Xms128M -Xmx256M

        3)  对resin容器,同样可以在启动时对jvm设置内存限度。在bin文件夹下创建一个startup.bat文件,内容如下:

@echo off
call "httpd.exe" "-Xms128M" "-Xmx256M"
:end

  • 优化程序,释放垃圾。

主要包括避免死循环,应该及时释放种资源:内存, 数据库的各种连接,防止一次载入太多的数据。导致java.lang.OutOfMemoryError的根本原因是程序不健壮。因此,从根本上解决Java内存溢出的唯一方法就是修改程序,及时地释放没用的对象,释放内存空间。 遇到该错误的时候要仔细检查程序,嘿嘿,遇多一次这种问题之后,以后写程序就会小心多了。

五、内存泄漏和内存溢出

另外,由于Java堆内也可能发生内存泄露(Memory Leak),这里简要说明一下内存泄露和内存溢出的区别:

  • 内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
  • 内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。Java中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,当我们new了对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这边会造成内存泄露,
  • 内存泄露是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
    memory leak会最终会导致out of memory!
  • 内存泄漏的情景:
public class Test {
    private static Map<String,Object> map = new HashMap<>();
    void doSomeThing(){
        Object object = new Object();
        object.toString();
        //把局部对象放到全局对象里
        map.put("haha", object);    
        //长生命周期的一个对象持有短生命周期的一个对象
        //map:长生命周期,object:短生命周期,不会被垃圾回收        
    }
}

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值