1. 常见的几种OOM
常见的OOM错误有以下几种:
java.lang.StackOverflowError
java.lang.OutOfMemoryError : java heap space
java.lang.OutOfMemoryError : GC overhead limit exceeded
java.lang.OutOfMemoryError : Direct buffer memory
java.lang.OutOfMemoryError : unable to create new native thread
java.lang.OutOfMemoryError : Metaspace
java.lang.OutOfMemoryError: Out of swap space
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
Out of memory:Kill process or sacrifice child
OOM(OutOfMemoryError) 问题归根结底三点原因:
- 本身资源不够
- 申请的太多内存
- 资源耗尽
解决思路,换成Java服务分析,三个原因也可以解读为:
- 有可能是内存分配确实过小,而正常业务使用了大量内存
- 某一个对象被频繁申请,却没有释放,内存不断泄漏,导致内存耗尽
- 某一个资源被频繁申请,系统资源耗尽,例如:不断创建线程,不断发起网络连接
因此,针对解决思路,快速定位OOM问题的步骤是:
- 确认是不是内存本身就分配过小
- 找到最耗内存的对象
- 确认是否是资源耗尽
1.1 java.lang.StackOverflowError
原因:在一个函数中调用自己会产生这个错误,函数调用栈太深了
代码示例:
//-Xms10m -Xmx10m
public class StackOverflowErrorDemo {
public static void main(String[] args) {
stackOverflowError();
}
private static void stackOverflowError() {
stackOverflowError();
//Exception in thread "main" java.lang.StackOverflowError
}
}
//控制台结果
Exception in thread "main" java.lang.StackOverflowError
解决方案:1.避免循环递归调用,2.调大栈的内存,3.内存泄露(Memory leak)
1.2 java.lang.OutOfMemoryError : java heap space
原因:1. 创建了一个很大的对象 2.超出预期的访问量/数据量,3.内存泄漏( Java中的内存泄漏, 就是那些逻辑上不再使用的对象, 却没有被 垃圾收集程序 给干掉. 从而导致垃圾对象继续占用堆内存中, 逐渐堆积)
代码示例:
//-Xms2m -Xmx2m -XX:+HeapDumpOnOutOfMemoryError
public class OutOfMemoryErrorJavaHeapSpaceDemo {
static final int SIZE=3*1024*1024;
public static void main(String[] a) {
int[] i = new int[SIZE];
}
}
//控制台结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
内存泄漏的例子
import java.util.*;
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map m = new HashMap();
while (true){
for (int i = 0; i < 10000; i++){
if (!m.containsKey(new Key(i))){
m.put(new Key(i), "Number:" + i);
}
}
System.out.println("m.size()=" + m.size());
}
}
}
粗略一看, 可能觉得没什么问题, 因为这最多缓存 10000 个元素嘛! 但仔细审查就会发现, Key 这个类只重写了 hashCode() 方法, 却没有重写 equals() 方法, 于是就会一直往 HashMap 中添加更多的 Key。
JAVA equals()与hashCode()方法之间的设计实现原则为:
如果两个对象相等(使用equals()方法),那么必须拥有相同的哈希码(使用hashCode()方法).
即使两个对象有相同的哈希值(hash code),他们不一定相等.意思就是: 多个不同的对象,可以返回同一个hash值.
hashCode()的默认实现是为不同的对象返回不同的整数.有一个设计原则是,hashCode对于同一个对象,不管内部怎么改变,应该都返回相同的整数值.
在上面的例子中,因为未定义自己的equals()实现,因此默认实现对两个对象返回两个不同的整数,这种情况破坏了约定原则。
ThreadLocal 缓存了当前线程所持有的 request 对象的java.lang.OutOfMemoryError : java heap space问题排查
1.3 java.lang.OutOfMemoryError : GC overhead limit exceeded
原因:执行垃圾收集的时间比例太大,有效的运算量太小,默认情况下,如果GC花费的时间超过98% 并且GC回收的内存少于2%,jvm就会抛出这个错误
代码示例:
//-Xmx10M -Xms10m -XX:MaxMetaspaceSize=10M
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
while(true){
list.add(UUID.randomUUID().toString().intern());
}
}
//Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
解决办法:1. 找到哪类对象占用了最多的内存,然后看是否增大堆内存,2. 需要进行GC turning
1.4 java.lang.OutOfMemoryError : Direct buffer memory
原因:主要是NIO 引起的,写NIO程序经常用到ByteBuffer来读取或者写入数据,这是一种基于通道和缓冲区的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在JAVA堆和native堆中来回切换。
ByteBuffer.allocate(capability) 这种方法是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对慢
ByteBuffer.allocateDirect(capability)这种方式是分配OS本地内存,不属于GC范围,由于不需要内存拷贝所以速度快。JVM执行GC不会回DirectByteBuffer收对象,这时JVM堆内存充足但是本地内存可能使用光了,再次分配本地内存就会出现Direct buffer memory
代码示例:
//-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
public static void main(String[] args){
System.out.println("maxDirectMemory : " + sun.misc.VM.maxDirectMemory() / (1024 * 1024) + "MB");
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
byteBuffer.putChar(‘a');
}
//输出
[GC (System.gc()) [PSYoungGen: 1315K->464K(2560K)] 1315K->472K(9728K), 0.0008907 secs] [Times: user=0.00 sys=0.00, real=
[Full GC (System.gc()) [PSYoungGen: 464K->0K(2560K)] [ParOldGen: 8K->359K(7168K)] 472K->359K(9728K), [Metaspace: 3037K->
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
解决办法一:
用来nio,但是direct buffer不够情况
1)检查是否直接或间接使用了nio,例如手动调用生成buffer的方法或者使用了nio容器如netty,jetty,tomcat等等;
2)-XX:MaxDirectMemorySize加大,该参数默认是64M,可以根据需求调大试试;
3)检查JVM参数里面有无:-XX:+DisableExplicitGC,如果有就去掉.
解决办法二:实例化client后没有关闭此对象
调用elasticsearch的client实例化对象后,要及时关闭客户端对象,没有关闭,造成实例化的对象越来越多,占用的内存也越来越多,最后内存就溢出了
/**
* 关闭client
*
* @param client
*/
public static void closeClient(Client client) {
if (client != null)
client.close();
client = null;
}
1.5 java.lang.OutOfMemoryError : unable to create new native thread
原因:1.应用创建了太多的线程,一个应用进程创建了多个线程,超过系统承载能力
2.你的服务器并不允许你的程序创建这么多线程,Linux系统普通用户默认单个进程可以创建的线程数是1024个
代码示例:
while(true){
new Thread(new Runnable(){
public void run() {
try {
Thread.sleep(10000000);
} catch(InterruptedException e) { }
}
}).start();
}
解决方案:
1.想办法降低你的应用程序创建的线程数量。
2. 对于确实需要创建很多线程的修改liunx配置,扩大Linux默认限制。
命令:ulimit -u 查看
vi /etc/security/limits.d/90-nproc.conf 增加一条用户记录配置大小
1.6 java.lang.OutOfMemoryError : Metaspace
原因:由于方法区被移至 Metaspace,所以 Metaspace 的使用量与 JVM 加载到内存中的 class 数量/大小有关。主要原因, 是加载到内存中的 class 数量太多或者体积太大导致元数据区(Metaspace) 已被用满。Java8 后使用元空间代替了永久代,元空间是方法区在HotSpot中的实现,它与持久代最大的区别是:元空间并不在虚拟机中的内存中而是使用本地内存。
元空间存放的信息:1. 虚拟机加载的类信息 ; 2 常量池;3.静态变量,4.即时编译后的代码
代码示例:
//-Xmx10M -Xms10m -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
public class Metaspace {
static javassist.ClassPool cp = javassist.ClassPool.getDefault();
public static void main(String[] args) throws Exception{
for (int i = 0; ; i++) {
Class c = cp.makeClass("Metaspace.demo.Generated" + i).toClass();
}
}
}
//Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
解决方案:
1. -XX:MaxMetaspaceSize=512m
但需要注意, 不限制Metaspace内存的大小, 假若物理内存不足, 有可能会引起内存交换(swapping), 严重拖累系统性能。 此外,还可能造成native内存分配失败等问题。
1.7 java.lang.OutOfMemoryError: Out of swap space
原因:往往是由操作系统级别的问题引起的,例如:
1.操作系统配置的交换空间不足。
2.系统上的另一个进程消耗所有内存资源。
3.还有可能是本地内存泄漏导致应用程序失败,比如:应用程序调用了native code连续分配内存,但却没有被释放。
解决方案:
第一种,升级机器以包含更多内存
也是最简单的方法, 增加虚拟内存(swap space) 的大小. 各操作系统的设置方法不太一样, 比如Linux,可以使用下面的命令设置:
swapoff -a
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile
第二种,优化应用程序以减少其内存占用
1.8 java.lang.OutOfMemoryError: Requested array size exceeds VM limit
原因:数组太大, 最终长度超过平台限制值, 但小于 Integer.MAX_INT。
代码示例:
for (int i = 3; i >= 0; i--) {
try {
int[] arr = new int[Integer.MAX_VALUE-i];
System.out.format("Successfully initialized an array with %,d elements.\n", Integer.MAX_VALUE-i);
} catch (Throwable t) {
t.printStackTrace();
}
}
//Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
此示例也展示了这个错误比较罕见的原因 —— 要取得JVM对数组大小的限制, 要分配长度差不多等于 Integer.MAX_INT 的数组. 这个示例运行在64位的Mac OS X, Hotspot 7平台时, 只有两个长度会抛出这个错误: Integer.MAX_INT-1 和 Integer.MAX_INT。
解决:
- 需要检查业务代码, 确认是否真的需要那么大的数组。如果可以减小数组长度,如果不行,可能需要把数据拆分为多个块, 然后根据需要按批次加载。
- 修改程序逻辑。例如拆分成多个小块,按批次加载; 或者放弃使用标准库,而是自己处理数据结构,比如使用 sun.misc.Unsafe 类, 通过Unsafe工具类可以像C语言一样直接分配内存。
1.9 Out of memory:Kill process or sacrifice child
原因:为了理解这个错误,我们需要补充一点操作系统的基础知识。操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,名叫“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer被激活,然后选择一个进程杀掉。哪一个进程这么倒霉呢?选择的算法和想法都很朴实:谁占用内存最多,谁就被干掉。
解决方案
解决这个问题最有效也是最直接的方法就是升级内存,其他方法诸如:调整OOM Killer配置、水平扩展应用,将内存的负载分摊到若干小实例上