导语
  内存一直是所有开发人员探索的一片天地,再JVM中,内存往往会被分为几块,了解不同的内存区域对编写出优质的代码有很大的帮助。堆内存作为JVM中比较重要的区域,有很多值得我们探索的地方。下面就来介绍一下Java堆内存溢出的解决思路

文章目录

  • 分析内存溢出的原因
    • 堆溢出
    • 直接内存溢出
    • 过多线程导致OOM
    • 永久区溢出
    • GC效率低下引起的OOM

分析内存溢出的原因

  OOM 作为比较令人头痛的问题一直困扰着很多程序员,它通常出现在某一块内存空间将要消耗尽的时候,在JVM中,导致内存溢出的原因也是比较多的,这里就来主要讨论一下最为常见的几种问题,包括堆溢出、直接内存溢出、永久区溢出等。

堆溢出

  Java堆作为JVM中最为重要的内存区域,由于大量的对象需要在堆内存进行分配,所以它是最有可能发生溢出的区域。大多数情况下的内存溢出都是堆内存溢出。通过上面描述可以知道,堆内存的溢出就是堆内存中出现大量的对象,没有及时的回收,占据了堆内存中的很大的空间,当对象大小之和大于由Xmx参数指定的堆空间大小的时候,溢出的错误就发生了。例如

public class SimpleHeapOOM{public static void main(String args[]){ArrayList<byte[]> list = new ArrayList<byte[]>();for(int i = 0;i<1024;i++){list.add(new byte[1024*1024]);}}
}

  运行上面的程序,马上就会抛出内存溢出的异常。Java heap space。

  为了减少堆溢出的错误,一方面可以使用-Xmx 参数指定一个大一点的堆空间,另一方面,由于堆空间不能无限的增加,通过MAT或者Visual VM等工具,找到大量占用堆空间的对象并在程序层面上做出合理的优化,这个是十分必要的。

直接内存溢出

  Java在NIO操作中支持使用直接内存,也就是通过Java代码可以额外获得一块堆外的内存空间,这个内存空间是直接向操作系统申请了,至于为什么会有这样一块内存空间,这个就跟NIO的原理有关了,这里就不多做说明了。直接内存的申请速度要比JVM的内存申请要慢,但是它的访问速度确实要比堆内存要快。所以这里就又得提到复用了,并且对于经常被访问的空间,使用直接内存可以提高系统的性能。但是由于直接内存没有被JVM所管理,所以使用不当的时候容易导致直接内存溢出。

public class DirectBufferOOM{public static void main(String args[]){for(int i = 0;i<1024;i++){ByteBuffer.allocateDirect(1024*1024);System.out.println(i)//System.gc();}}
}

  运行上述代码,不用多久就会出现内存溢出,导致程序退出。
  当然读者实验的时候可以稍微的降低JDK的版本,降低操作系统的配置,可能在高性能的机器上,这种代码不会出现任何的问题。
  或许有人会问,通过这种方式,JVM的垃圾回收机制为什么没有起作用呢?在程序的第四行,分配的直接内存并没有被任何对象引用,为什么没有被垃圾收收集器回收呢?可以加入如下的参数再次运行

-XX:+PrintGCDetails

  会发现并没有垃圾收集日志打印出来,也就是说整个的执行过程并没有涉及到GC操作。实际上直接内存不一定触发垃圾回收操作,除非是直接内存使用量达到了-XX:MaxDirectMemorySize的设置值。所说保证直接内存不溢出的方式就是可以合理的进行Full GC。或者设定一个系统可以达到的-XX:MaxDirectMemorySize值,默认是-Xmx。因此,如果系统的堆内存少有GC发生,而直接内存申请频繁,会比较容易导致直接内存溢出,这个在32位机器上比较明显,64位机器上除非是极限情况。

  上述代码第六行代码,去掉注释,使得显式的GC生效,那么整个程序就可以正常的结束,这个就说明GC是可以回收直接内存的。

  而另一个让上述代码正常执行的操作就是设置一个较小的堆,在不指定–XX:MaxDirectMemorySize的情况下,最大可以使用的直接内存是堆内存的大小。

-Xmx512M -XX:+PrintGCDetails

  这里将最大堆内存限制在512M,这种情况下,最大可用直接内存也是512M。操作系统可以同时为堆和直接内存提供足够的空间,当直接内存使用达到512M的时候,也会进行GC操作,释放无用的空间。

  显式设置-XX:MaxDirectMemorySize也是解决这个问题的方法,只要设置一个系统实际可以达到的最大直接内存值,实际上不应该触发的内存溢出就不会发生。

  综上,为了避免直接内存溢出,在确保空间不浪费的基础上,合理执行显式GC可以降低直接内存溢出的概率,设置合理的-XX:MaxDirectMemorySize大小值,也可以避免直接内存溢出。

过多线程导致OOM

  由于为每个线程开辟空间,都要占用内存,因此当线程数量太多的时候,就可能导致OOM问题。由于线程的栈空间也是在堆外分配的,所以与直接内存相似,如果想让系统支持更多的线程,那么应该使用一个较小的堆内存。

public class MultiThreadOOM{public static class SleepThread implements Runable{public void run(){try{Thread.sleep(100000000)}catch(InterruptedException e){e.printStackTrace();}}}public static void main(String args[]){for(int i = 0;i<1500;i++){new Thread(new SleepThread(),"Thread - "+i).start();}}
}

  运行上述代码可以知道,可以看到控制台打印 unable to create new native thread ,标识系统创建线程的数量已经饱和,原因是Java进程已经达到了内存使用的上限。要解决这个问题,从如下两个方面下手。

  • 1、尝试减少堆空间,可以使用-Xmx 进行设置
  • 2、减少每个线程所占用的内存空间,使用-Xss 参数可以指定线程的栈空间

注意 如果减少线程的栈空间大小,栈溢出的风险就会相应上升
  综上,处理这一类的OOM,除了合理的减少线程总数,减少最大堆空间数、减少线程栈空间也是可行的,但是需要考虑到栈溢出风险。

永久区溢出

  永久区(Perm) 是存放类元数据的区域。如果一个系统中有太多的类型,那么就会导致永久区溢出。在JDK1.8中,永久区改为叫做元数据区,但是它与永久区的功能是类似的,都是为了保存类的元数据。

  如果一个系统不断的产生新的类,没有回收,最终非常有可能导致永久区溢出。如下,这段代码每次都会循环生成一个新的类,是类,而不是对象实例。

public class PermOOM{public static void main(String[] args){try{for(int i =0;i<100000;i++){CglibBean bean = new CglibBean("com.test.bean"+,new HashMap());}}catch(Error e){e.printStackTrace();}}
}

  博主为了测试是使用JDK1.6,并且使用了如下的参数执行上述代码

-XX:MaxPermSize=5M

  程序运行一段时间之后,抛出 PermGen space 异常

  一般来说,要解决永久区溢出的问题,可以从如下的几个方面来考虑

  • 1、增大MaxPermSize的值。
  • 2、减少系统需要的类的数量。
  • 3、减少系统需要的类的数量
  • 4、使用ClassLoader合理地装载各个类,并定期进行回收。

GC效率低下引起的OOM

  GC作为内存回收的关键,如果GC效率低下,那么整体的系统性能都会受到影响。如果系统的堆空间太小,那么GC话费的时间就会比较多,并且回收释放掉的内存会较少。

  根据GC占用的系统时间,以及释放内存的大小,虚拟机就会评判GC的效率,一旦效率过低,就会直接抛出OOM。但JVM一般情况下不会随意判断,因为即使GC效率不高,强制终止程序也是不合适的,一般会检查如下的几种情况

  • 1、花在GC上的时间是否超过98%
  • 2、老年代释放的内存是否小于2%
  • 3、eden区释放的内存是否小于2%
  • 4、是否连续5次GC都出现了上述情况

  当满足条件的时候,才会抛出OOM异常 GC overhead limit exceeded

  尽管虚拟机限制的很严格,但是再大部分的使用场景中还是会有堆溢出的异常。这个OOM只起到辅助的作用,提示堆内存太小,因此虚拟机也不是非要开启这个提示,可以通过关闭-XX:-UseGCOverheadLimit来禁止上述问题的出现。

垃圾回收算法与实现系列-Java堆内存溢出原因相关推荐

  1. 垃圾回收算法与实现系列-Java的Class文件详解

    导语   对于JVM来说,Class文件作为虚拟机的一个重要接口.无论使用什么样的语言,进行软件开发,只要能将源码编译为Class文件,并放到正确的路径下,那么这种语言就可以被JVM所执行.可以说Cl ...

  2. Java堆内存溢出解决方案

    Java堆内存溢出的问题 引言 堆内存工作原理 移除永久代? 分代是什么? 为什么分代? 为什么Survivor分为两块相等大小的幸存空间? JVM堆内存常用参数 垃圾回收算法 垃圾收集器 串行收集器 ...

  3. 模拟JAVA堆内存溢出和栈内存溢出

    文章目录 1. 模拟堆内存溢出 2. 模拟栈内存溢出 1. 模拟堆内存溢出 为了更快的出现堆内存溢出,可以限制Java堆的大小为10MB(不限制也可以).代码如下(可直接复制使用): package ...

  4. java堆内存溢出的一般原因是什么_中软国际:Java堆内存溢出的本质是什么

    了解内存溢出错误的本质 事实证明,无论是什么情况,只要了解它的基本情况比如基本概念,解决起来相对得心应手些.如何去评估和了解一个内存溢出错误?最先做的事情应该是观察内存增长特征.根据情况做出可能性的评 ...

  5. 垃圾回收算法与实现系列-学习GC之前的准备工作

    导语   在学习垃圾回收算法之前,首先需要了解什么是Heap.什么是Root.什么是Object.什么是Stack.什么是Pointer,这写概念都是什么,为什么要在垃圾回收算法中使用,使用这些东西有 ...

  6. Java堆内存溢出代码示例

    不断创建对象会导致堆内存溢出:

  7. 总结Java程序内存溢出原因

    目录 内存溢出和内存泄漏 直接内存溢出 堆溢出 方法区溢出 这篇日志总结下Java程序中的发生内存溢出的一些原因,我们知道JVM堆空间十分重要,大部分对象在创建时都是放在堆中(除了一些逃逸对象是栈上分 ...

  8. 垃圾回收算法与实现系列-GC 标记-清除算法

    导语   在GC 中最重要的算法就是GC标记-清除算法(Mark-Sweep GC).在很多的场景下都还是在使用这个算法来进行垃圾回收操作.就如如同它的名字一样先标记,然后清除.下面就来看看标记清除算 ...

  9. 垃圾回收算法与实现系列-String在虚拟机中的实现

    导语   String 字符串一直作为各种编程语言的核心内容存在.作为动态字符的一种是实现方案,应用很广泛.每一种计算机语言对于这种数据结构都进行了特殊的优化和实现.在Java中,String作为引用 ...

最新文章

  1. A Strange Bitcoin Transaction
  2. 如何利用Gephi可视化浏览的网站关系
  3. KDE与GNOME的起源与发展
  4. 【alibaba-cloud】openfeign的使用
  5. 《怪物猎人》战斗核心设计分析
  6. hadoop搭建_hadoop分布式搭建之虚拟机克隆
  7. 命中率_三分命中率暴涨19%!卡皇进化已无弱项,顶级3D练成何须布拉
  8. HDU 1203 I NEED A OFFER!(01背包)
  9. javascript好文---深入理解定位父级offsetParent及偏移大小
  10. pytorch 回归预测(时间序列)
  11. C#/Net的语法和一些小技巧笔记
  12. 计算机中那些事儿(九):资料管理一些建议---理论篇
  13. hdu 敌兵布阵(线段树之单点更新)
  14. 我们和优秀工程师的差距在哪儿
  15. 利用Python网络爬虫抓取韩寒博客推荐第一篇(urllib的简单使用与Beautifulsoup实战,i/o编程)
  16. 股市风起云涌,我用Python分析周期之道
  17. 打开chm文件c语言,chm格式,手把手教你chm文件怎么打开
  18. 【codevs 1329】东风谷早苗
  19. 思科2960交换机密码破解
  20. 推荐书籍:RNA甲基化表观转录组学

热门文章

  1. php怎样使用pdo,PHP中使用PDO_PHP教程
  2. mysql jdbc 多数据源_springboot jdbc连接多个数据源
  3. 几百万的数据查找重复值_如何快速查找出Excel中的重复数据,多角度分析
  4. SpringBoot 自动装配原理
  5. Vue+Vue-router+Vuex项目实战
  6. 事务并发、事务隔离级别
  7. 使用C#和Excel进行报表开发(5)
  8. LPI 认证考试介绍
  9. kafka分区停留在UnderReplicated状态
  10. Java之品优购课程讲义_day16(2)