在java编程中,内存的问题要么不是问题,要么就是极大的问题。
让我们来看看这些问题包括什么?首先,一个会遇到但是比较难缠的问题是OOM(OutofMemory),跟OOM可能相关的问题是内存泄露;另一个关于内存的问题是GC(garbage collection)。
我们为什么要关注OOM呢?比如一个简单的例子,如果遇到这样一个OOM异常,我们能大致判断出来是什么问题吗?
java.lang.OutofMemoryError: null如果性能过一段时间就莫名其妙下降,然后又莫名其妙恢复,如此往复,我们能大致判断出来是跟GC有关系吗?
at sun.misc.Unsafe.allocateMemory(Native Method)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java)
接下来我们就来聊聊这两个问题。对于OOM问题,其基础莫过于jvm虚拟机内存的分配和占用了。按照《深入理解JAVA虚拟机》一书中的描述(如需详细内容,可以参考该书),jvm内存应该如下:
PC就是运行时的代码指针,与硬件中的PC是一样的,只不过在虚拟机中需要模拟这样一个程序的指针。
方法区是比较重要的区域,这里存放相对变动较少的内容,比如,常量,ClassLoader加载的类信息,静态变量等等,有的虚拟机实现用它存放GC时比较老的对象(永远不死型,spring就是一个喊着"春"的不死型怪物)
栈一般存放方法调用的上下文,用来保存现场,以便后面恢复。
堆一般存放我们创建的对象,与GC密切相关,后面会专门讲解这部分。
另外就是本地方法栈,当我们使用JNI或者调用本地方法时的存放地方。
还有直接内存,比较容易被忽视,因为它并不在jvm的内存区域,而是在系统内存减去jvm内存的剩余部分。在NIO中为了提高性能使用的直接内存比如DirectByteBuffer,就是存在这个部分,由于不在jvm中,爆出问题经常想不到。
有了如上一个基本认识之后,从不同部位报出来的OOM,就很容易分辨了。
比如看到*Error,十有八九是递归调用了,因为是方法调用上下文溢出。
看到java.lang.OutofMemoryError:PermGen space,注意后面的的PermGen space(即方法区或永久代),一般出现在载入的类过多,比如,我们部署一个应用时,如果依赖jar包过多,永久代区域设置过小,则会发生这类OOM(更常见是出现在tomcat热部署时,如果已经部署了jar包很多的应用A,此时再次热部署A,很容易看到这个问题)。按照我的理解,是因为永久代不会被GC到,第一次部署已经耗费了大部分永久代内存,再次部署时很可能就让它崩了。
一般的java.lang.OutOfMemoryError:java heap space实际上还是比较容易发生的,比如一个消息队列,如果处理速度过慢,生产速度过快,长期积累就容易导致堆溢出;另外,如果缓存写得不够好,无限制占用jvm内存又没有用Soft, Weak这类的引用在适当时候释放内存,都容易导致这个异常。
说到这里,可以聊聊内存泄露,实际上内存泄露跟溢出是两个概念,泄露未必溢出(可能每次gc都能够回收部分内存因而不会溢出),溢出也未必泄露(可能由于分配的内存本身就不足)。内存泄露发生的场景大体上有这么几个:一个是不正确的缓存,参考这里;一个是事件的Listener只注册而没有删除;还有一个《effective java》中说的错误的Stack写法;还有一个我遇到的jdbc的误用,参考这里
另外还有一种常见的OOM,类似于java.lang.OutOfMemoryError:at sun.misc.Unsafe.allocateMemory(Native Method),一般常见于NIO的运用及相关类库(许多开源类库有这个问题,因此更加难调试,比如Comet,甚至著名的Mina),碰到这个问题,首先要想想整个系统中哪几个地方用到了NIO,从这几个地方做切入会事半功倍。
jvm参数调优
许多程序的OOM都是程序编写的问题,但是,还有一部分是程序的确很大,需要超过默认值的内存,这个时候该如何呢?下面介绍一些常见的调整这些不同区域内存大小的参数。
堆大小:-Xms64M -Xmx256M,分别调整堆的最大最小值
方法区大小:-XX:PermSize=10M -XX:MaxPermSize=60M,分别调整方法区的最大最小值
栈大小:-Xss512k,调整栈的大小
设置年轻代内存的大小:-Xmn2g,设置年轻代大小为2g,增大年轻代将会减小老年代的大小
年轻代与老年代的比例(即Eden+survivor / old):-XX:NewRatio=4,设置年轻代与老年代的大小比值,设置为4的话,即年轻代为4,老年代为1
设置Eden区与survivor区比例:-XX:SurvivorRatio=4,则eden为4,from survivor为1,to survivor为1。
更多jvm参数调优请参考这里:http://kenwublog.com/docs/java6-jvm-options-chinese-edition.htm
接下来的问题就是GC问题,要想了解GC问题,首先要了解GC常见算法,这样就能明白整个内存为什么这样设计。开始之前,先上一张图(来源于互联网),show一下关于gc要用到的区域。
可以看到,关于java中对象的存放,分为年轻代,老年代,持久代,年轻代又继续划分为Eden区,From survivor区,To survivor区。年轻代和老年代都放在堆中,比较有争议的是持久代到底放在哪里,这要视乎虚拟机的实现了,有的虚拟机将持久代放到方法区,有的虚拟机将持久代还是放到堆中,这个不是重点。
为什么这么划分区域?这得从垃圾收集算法讲起。作为垃圾收集算法,要考虑几个重点,一是判断哪些对象该被回收,二是如何回收,三是何时回收。
判断哪些对象该被回收
最早出现的判断哪些对象该被回收的方法是引用计数法。其思想是每个对象有个计数器,在对象增加一个引用时计数器加一,当离开方法作用区域或者引用置空时减一,如果某个时刻对象引用计数器为0,则需要回收。这种方法非常简单,但是有个缺点,即无法判断循环引用,如图,运行完一个方法后,A,B两个对象已经离开方法作用域,应该被回收,但是他们仍然互相引用,即A, B此时应用计数器都为1从而无法回收造成泄漏和浪费。
改进的方法是根收集算法,其思想是从根对象开始出发,能够达到的所有对象即为活动对象,不能被回收,不能达到的对象则是死亡对象,待回收,如此很好的解决了循环引用的问题。但是每次运行一遍该算法实际上是比较耗资源的,可以想象,系统中成千上万对象,每次计算都会占用时间和cpu资源,因此,何时回收的问题就比较重要了。这个问题我们留到讲解完如何回收再做解释。
如何回收
如何回收主要考虑的是用什么方式清除对象,这个地方需要考虑内存空间的利用率。
最容易想到的方式就是用上面的算法找到所有不可达对象,然后mark他们,并且清除他们,这种方式有个比较大的缺陷,就是内存碎片,如图,红色表示未回收,绿色表示已经回收,如果需要申请10个单位的连续空间,则不能满足要求,但是实际上是存在这么大空间的。
于是可以用另一个方式,即复制。首先将内存区域分为两块,只使用其中一块,当需要收集时,简单的将活动对象(红色)拷贝到另一块去,如此一来就完成了回收和整理的过程。其缺点是空间利用率不高,但是因为其实现简单,运行效率也较高(以空间换时间的代价还不高效就悲剧了),被各大商业虚拟机广为使用。
但是由于其浪费空间,所以还可以想到另一种方式,即标记整理,这种方式跟标记清除一样,首先标记清除,然后将空间整理一次,最终得到下图。但是其算法效率较低(需要整理)
从对象生命周期的特性能够知道,年轻对象大多“朝生夕灭”,年老对象大多“越活越久”。可以推断处理啊年轻对象的垃圾收集需要频繁回收,并且很可能100个回收之后只剩下10个;而年老对象正相反,需要少回收,并且经常100个回收还剩90个。
正因为对象的这种特性,我们可以发现年轻代可以用复制算法,一是高效,适应它经常运行的特性;二是回收后能空出大量空间来,不会因为内存一分为二而空间不足。而年老代可以用标记整理算法,因为效率低,所以不能经常运行;另外很可能计算了半天也没真正删除几个。
所以最终收集的方式为:虚拟机频繁针对年轻代运行minor gc,第一次将Eden复制到From区,第二次将Eden和From区复制到To区,第三次将Eden和To区复制回From区,不断往复。一般Eden与survivor比例为8:1
而针对老年代运行Full gc,采用的就是标记整理算法,相对较慢。
何时回收
实际上上面已经回答了这个问题,值得注意的是虚拟机在OOM前一般会做一次GC,如果内存仍然不足则抛出OOM
gc面试 http://hi.baidu.com/b__a__i__d__u/item/b5c28e4396669c16886d1063
java gc http://cwfmaker.iteye.com/blog/2255939
监控Java垃圾回收 http://shellblog.sinaapp.com/?p=701
java内存泄露的理解和解决 http://www.blogjava.net/zh-weir/archive/2011/02/23/345007.html
java.lang.OutOfMemoryError:PermGen space 或者Java heap space的产生原因以及解决办法 http://blog.csdn.net/hurryjiang/article/details/7946190
HotSpot JVM就是个庞氏骗局:http://it.deepinmind.com/gc/2014/04/01/hotspot-jvm-ponzi-scheme.html
oom大全 http://blog.csdn.net/spidercoco/article/details/20459095