一、内存溢出的原理
在C/C++语言中,需要开发者自己管理内存,通过malloc/new分配内存空间,并且需要写配对的delete/free代码。
在Java语言中,由Java虚拟机管理内存。
冯诺伊曼结构
Java虚拟机运行时数据区
Java线程共享空间
二、哪些区域会内存溢出
1.Java堆
内存溢出例子
public class OutOfMemoryTest {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<byte[]>();
for (int i = 0; i < 10240; i++) {
list.add(new byte[1024 * 1024]);
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Minor GC 会清理年轻代的内存
Major GC 是清理老年代
Full GC 是清理整个堆空间—包括年轻代和老年代
在新生代中,每次垃圾收集时都会发现大批对象死去,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代因为对象存活率高,没有额外空间对它进行分配担保,就需要使用“标记-清理”或者“标记-整理”算法来进行回收。CMS是基于“标记-清除”算法实现的收集器。
2.永久代
用语存储类信息、常量、静态变量、即使编译器编译以后的代码
永久代的垃圾收集主要回收两部分内容:废弃的常量和无用的类。
无用的类:类需要同时满足下面3个条件才能算是“无用的类”:
1.该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。
在大量使用反射、动态代理、CGlib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出
3.Direct Memory
NIO引入基于通道和缓冲区的I/O方式,使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作
4.线程堆栈
每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
5.Socket缓冲区
每个Socket连接都有Receive和Send两个缓冲区
6.JNI代码
7.虚拟机和GC
三、哪些情况下会内存溢出
1.内存中加载的数据量过于庞大
一次从数据库取出过多数据
使用BlockingQueue无界队列,消费者速度小于生产者速度
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
3.代码中存在死循环或循环产生过多重复的对象实体
4.使用的第三方软件中的BUG
5.启动参数内存值设定的过小
-Xms :初始堆大小
-Xmx :最大堆大小
-Xmn:新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor。
-XX:NewSize=n :设置年轻代
-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。Survivor区有两个。如果为3,表示Eden:Survivor=3:2
-XX:PermSize=n 永久代(方法区)的初始大小
-XX:MaxPermSize=n :设置永久代大小
-Xss 设定栈容量;
-XX:PretenureSizeThreshold (该设置只对Serial和ParNew收集器生效) 可以设置进入老生代的大小限制
-XX:MaxTenuringThreshold=1(默认15)垃圾最大年龄 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
该参数只有在串行GC时才有效.
-XX:MaxDirectMemorySize 设置本机直接内存大小
6.StackOverflow
关于虚拟机栈和本地方法栈,Java虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
7.递归次数过多
8.内存泄漏
1.ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏
2.
LinkedBlockingQueue
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
如果没有first.item = null;那么对象就不会被回收,造成内存泄漏
3.数据库连接,网络连接和io连接完成以后没有调用close()方法。
9.线程过多。
四、哪些习惯可以尽量避免内存溢出
1、尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。
对于仍然有指针指向的实例,jvm就不会回收该资源,因为垃圾回收会将值为null的对象作为垃圾,提高GC回收机制效率;
2、我们的程序里不可避免大量使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域;
String str = "aaa";
String str2 = "bbb";
String str3 = str + str2;
假如执行此次之后str ,str2以后再不被调用,那它就会被放在内存中等待Java的gc去回收,建议在使用字符串时能使用StringBuffer就不要用String,可以省不少开销。
3、尽量少用静态变量,因为静态变量是全局的,GC不会回收的。
4、避免集中创建大对象,JVM会突然需要大量内存,这时必然会触发GC优化系统内存环境
使用jspsmartUpload作文件上传,运行过程中经常出现java.outofMemoryError的错误
检查之后发现问题:组件里的代码
m_totalBytes = m_request.getContentLength();
m_binArray = new byte[m_totalBytes];
问题原因是totalBytes这个变量得到的数极大,导致该数组分配了很多内存空间,而且该数组不能及时释放。
5、尽量运用对象池技术以提高系统性能;生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。
6、尽量不在经常调用的方法中创建对象,尤其是在循环中创建对象。可以适当的使用Hashtable创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃。
7、开启大型文件或跟数据库一次拿了太多的数据,造成 Out Of Memory Error 的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
select * from information_schema.TABLES
where information_schema.TABLES.TABLE_SCHEMA='databasename'
and information_schema.TABLES.TABLE_NAME='tablename'
五、如何检测内存溢出
1.监测内存使用率,当内存使用率达到阈值时报警,人工介入。
2.jmap命令用于生成堆转储快照,jstack命令用于生成虚拟机当前时刻的线程快照。
3.可视化工具JConsole和VisualVM、Mission Control、MAT