内存溢出(OutOfMemoryError):应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。
(1)java.lang.OutOfMemoryError: PermGen space
原因:一般是程序中web-inf/lib下引入了大量的第三方Jar或class,使java虚拟机装载类的空间不够,JVM中永久代(Permanent Generation space)如果不进行配置的话,默认的大小为4M。这种问题经常出现第一次部署Tomcat到服务器上的时候。
解决方案(建议):TOMCAT_HOME/bin/catalina.sh在
echo "Using CATALINA_BASE: $CATALINA_BASE"
上面加入以下行:
JAVA_OPTS="-server -XX:PermSize=64M -XX:MaxPermSize=128m"
建议:如果tomcat部署了多个应用,很多应用都使用了相同的jar,可以将共同的jar移到 tomcat共同的lib下,减少类的重复加载。例如:将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以达到减少jar 文档重复占用内存的目的。但是,Tomcat部署多个应用,他们将运行在同一个JVM上,共享JVM的配置,那么就存在一个应用把JVM搞瘫痪之后,其他应用也无法运行的情况。所以重要的应用不建议部署在同一个Tomcat下。
(2)java.lang.OutOfMemoryError: Java heap space
直接原因:java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap space有关。
那么虚拟机创建的对象太多,是怎么引起的呢?本质原因可能有常见以下的情况:
针对上述的原因就会有对应的解决方案*(建议):
关于数据库查询的问题,笔者最近在生产上就看到了这样的例子,一个查询功能的页面,查询条件就汇款流水号一个字段,这个字段没有做非空校验,由于这个功能的SQL主要是根据流水来过滤的,也就是说如果没有填写流水,几乎相当于全表查询。一开始上线的时候,表中的数据量不大,业务使用功能的时候一般也会填写流水号,一直到一段时候之后,这张表的数据量达到了数十W,业务没有输入流水号,点击了查询,系统直接内存溢出,只能重启服务。
(1)java.lang.OutOfMemoryError: PermGen space
我们需要的是设置方法区的大小,实现方式是通过设置-XX:PermSize和-XX:MaxPermSize参数,这样可以快一点达到我们的目的。
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。在 JDK 1.6 及之前的版本中,由于常量池分配在永久代内String.intern() 方法是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。可以通过死循环调用String.intern()来使得方法区内存溢出。
import java.util.ArrayList;
import java.util.List;
public class RuntimeConstantPoolOom {
public static void main(String[] args) {
List list = new ArrayList();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
JDK1.7以及之后的版本,常量池被移出永久代,我们可以通过不停的加载class来模拟方法区内存溢出,《深入理解java虚拟机》中借助 CGLIB 这类字节码技术模拟了这个异常,我们这里使用不同的 classloader 来实现(同一个类在不同的 classloader 中是不同的),如下:
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;
public class MethodAreaOom {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {
Set> classes = new HashSet>();
URL url = new File("").toURI().toURL();
URL[] urls = new URL[]{url};
while (true) {
ClassLoader loader = new URLClassLoader(urls);
Class> loadClass = loader.loadClass(Object.class.getName());
classes.add(loadClass);
}
}
}
在 jdk1.8 上运行上面的代码将不会出现异常,因为 jdk1.8 已结去掉了永久代,当然 -XX:PermSize=2m -XX:MaxPermSize=2m 也将被忽略。
(2)java.lang.OutOfMemoryError: Java heap space
同样的,我们设置jvm堆内存的大小, -Xms(堆的最小值),-Xmx(堆的最大值)为5M。
import java.util.ArrayList;
import java.util.List;
import testbean.UserBean;
public class HeapOOM {
public static void main(String[] args) {
List users = new ArrayList();
while (true) {
users.add(new UserBean());
}
}
(3)java.lang.StackOverflowError
我们在重现异常的时候就要相应的将栈内存容量设置的小一些,设置栈大小的方法是设置-Xss参数,-Xss=100K
public class VMStackOOM {
public static void main(String[] args) {
Recursion recursion = new Recursion();
try {
recursion.recursionself();
} catch (Throwable e) {
System.out.println("current value :" + recursion.currentValue);
throw e;
}
}
}
public class Recursion {
public int currentValue = 0;
public void recursionself() {
currentValue += 1;
recursionself();//递归调用
}
}
(4)直接内存溢出
在Java的NIO(New IO)中,支持直接内存的使用,也就是通过Java代码,获得一块堆外的内存空间,这块空间是直接向操作系统申请的。直接内存的申请速度一般要比堆内存慢,但是其访问速度要快于堆内存。因此,对于那些可复用的,并且会被经常访问的空间,使用直接内存是可以提高系统性能的。但由于直接内存没有被Java虚拟机完全托管,若使用不当,也容易触发直接内存溢出,导致宕机。以下的程序在32位虚拟机上运行:
public class DirectBufferOOM {
public static void main(String args[]){
for(int i=0;i<1024;i++){
//申请直接内存,不占用JVM虚拟的内存,与之对应的是
//public static ByteBuffer allocate(int capacity)申请的是JVM的内存
ByteBuffer.allocateDirect(1024*1024);
System.out.println(i);
// System.gc();
}
}
}
直接内存不一定能够触发GC(除非直接内存使用量达到了-XX:MaxDirectMemorySize的设置),所以保证直接内存不溢出的方法是合理地进行Full GC的执行,或者设定一个系统实际可达的-XX:MaxDirectMemorySize值(默认情况下等于-Xmx的设置)。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。memory leak会最终会导致out of memory!
以发生的方式来分类,内存泄漏可以分为4类:
举个例子:项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种情况一般是代码中出现了缓慢的内存泄漏,这就需要使用内存查看工具了。
内存查看工具有许多,比较有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它们的基本工作原理大同小异,都是监测Java程序运行时所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有内存泄漏问题。一般来说,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不应该是无限制的增长的。持续地观察系统运行时使用的内存的大小,可以看到在内存使用监控窗口中是基本规则的锯齿形的图线,如果内存的大小持续地增长,则说明系统存在内存泄漏问题。通过间隔一段时间取一次内存快照,然后对内存快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏。
参考资料: