JVM有关的知识点(四)

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) 问题归根结底三点原因:

  1. 本身资源不够
  2. 申请的太多内存
  3. 资源耗尽

解决思路,换成Java服务分析,三个原因也可以解读为:

  • 有可能是内存分配确实过小,而正常业务使用了大量内存
  • 某一个对象被频繁申请,却没有释放,内存不断泄漏,导致内存耗尽
  • 某一个资源被频繁申请,系统资源耗尽,例如:不断创建线程,不断发起网络连接

因此,针对解决思路,快速定位OOM问题的步骤是:

  1. 确认是不是内存本身就分配过小
  2. 找到最耗内存的对象
  3. 确认是否是资源耗尽

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。

解决:

  1. 需要检查业务代码, 确认是否真的需要那么大的数组。如果可以减小数组长度,如果不行,可能需要把数据拆分为多个块, 然后根据需要按批次加载。
  2. 修改程序逻辑。例如拆分成多个小块,按批次加载; 或者放弃使用标准库,而是自己处理数据结构,比如使用 sun.misc.Unsafe 类, 通过Unsafe工具类可以像C语言一样直接分配内存。

1.9 Out of memory:Kill process or sacrifice child

原因:为了理解这个错误,我们需要补充一点操作系统的基础知识。操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,名叫“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer被激活,然后选择一个进程杀掉。哪一个进程这么倒霉呢?选择的算法和想法都很朴实:谁占用内存最多,谁就被干掉。

解决方案
解决这个问题最有效也是最直接的方法就是升级内存,其他方法诸如:调整OOM Killer配置、水平扩展应用,将内存的负载分摊到若干小实例上

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值