题记:说好的坚持一周两篇文章在无数琐事和自己的懒惰下没有做好,在此表达一下对自己的不满并对有严格执行力的人深表敬意!!!!
---------------------------------------------------------------------------------------------------------------------------------
引文:Java程序员对OutOfMemory并不陌生,一般来说,出现此异常主要是由于应用里缓存了大量的数据没有被GC掉导致堆内存溢出,可是很多时候,为了减少重复计算或提升运行速度,必需要将一些数据缓存起来,比如启动的时候加载配置文件信息、从数据库里初始化进来的信息、运行过程中得到的一些中间结果等。程序员往JVM里加载这些数据的时候往往会很纠结,一方面想缓存的越多越好,尽量减少查库和重复计算,但另一方面过多的缓存对GC造成压力,甚至要提心吊胆的考虑溢出的问题。
需求:如果能有一种方法可以尽可能的缓存数据提高运行效率,又可以在GC前主动清空一部分过期数据从而防止内存溢出,该有多好。下面,我要讲的基于Java软引用实现堆内存监控,是笔者亲身在生产系统的实践,或许可以帮助程序员在这方面做一些尝试。
导读:文章会先解释什么是软引用,接着会说明GC对软引用的处理特点,围绕其特点利用JDK自带的相关类阐述代码实现细节。
正文:
1. 什么是软引用:
我们知道,Java中有四种引用关系,分别是强引用、软引用、弱引用、虚引用,如下图:
强引用:指JVM内存管理器从根引用集合(ROOt Set)出发遍寻堆中所有可到达的对象引用关系,也是常用的引用类型,如Object obj = new Object();只要强引用存在则GC时则必定不被回收。
软引用:用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
弱引用:用来描述非必需对象的,在java中,用java.lang.ref.WeakReference类来表示。当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
虚引用:在任何时候都可能被垃圾回收器回收的对象应用,用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,此引用关系更多的是与虚引用队列相关联以方便做一些GC监控。
2. 软引用特点
根据java帮助文档:"软引用对象在响应内存需要时,由垃圾回收器决定是否清除此对象。软引用对象最常用于实现内存敏感的缓存。假定垃圾回收器确定在某一时间点某个对象是软可到达对象。这时,它可以选择自动清除针对该对象的所有软引用,以及通过强引用链,从其可以到达该对象的针对任何其他软可到达对象的所有软引用。在同一时间或晚些时候,它会将那些已经向引用队列注册的新清除的软引用加入队列。软可到达对象的所有软引用都要保证在虚拟机抛出 OutOfMemoryError 之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。此类的直接实例可用于实现简单缓存;该类或其派生的子类还可用于更大型的数据结构,以实现更复杂的缓存。只要软引用的指示对象是强可到达对象,即正在实际使用的对象,就不会清除软引用。例如,通过保持最近使用的项的强指示对象,并由垃圾回收器决定是否放弃剩余的项,复杂的缓存可以防止放弃最近使用的项。"
根据上述内容我们知道,软引用对象会在OutOfMemoryError 之前由JVM保证将其回收,并把它加入到其注册的清除队列中。因此,通过监控该队列是否有即将被清除的软引用对象,我们就可以间接得知java应用是否已经到溢出崩溃边缘了,并在其溢出前迅速执行部分缓存数据的清空工作从而让虚拟机可以清理出一些内存出来避免堆内存的溢出,更进一步想,我们可以将该软引用对象设置成占一定内存大小的对象,如10M,这样当虚拟机内存不足时会第一时间将此对象回收进而腾出10M空余内存,进而缓解内存不足,同时为应用争取了宝贵清空部分缓存数据的时间,有效避免直接抛出内存溢出的异常。
3. 实现细节
根据上面的分析和实际的开发实践,利用软引用对象监控虚拟机内存使用情况的代码实现如下:
- 初始化软引用对象与引用队列,并设置软引用对象占用10M的内存
1 //内存监控 2 public static ReferenceQueue<byte[]> memoryDetectorQueue ; 3 public static SoftReference<byte[]> memoryDetector; 4 5 // initial 6 public static void initial(){ 7 memoryDetectorQueue = new ReferenceQueue<byte[]>(); 8 memoryDetector = new SoftReference<byte[]>(new byte[(int)(10*1024*1024)],memoryDetectorQueue); 9 }
- 设置一个单独的线程,并在软引用对象初始化后启动该线程,开始监视memoryDetectorQueue是否非空,非空则说明软引用对象由于内存空间不够被清理,内存告急:
1 public class MemoryMonitorService implements Runnable { 2 3 public void run() { 4 while (true) { 5 try { 6 if (memoryDetectorQueue.remove() != null) { 7 doPartClean(); //执行部分缓存的清空以释放内存,可以根据一些LRU算法或按比例来执行清理 8 } 9 } catch (Exception e) { 10 logger.error("", e); 11 }finally{ 12 memoryDetector = new SoftReference<byte[]>(new byte[(int) (10 * 1024 * 1024)], 13 memoryDetectorQueue); // 执行完部分缓存清理后重新创建软引用对象 14 } 15 } 16 } 17 }
说明:memoryDetectorQueue.remove()方法会一直等待,阻塞到某个对象变得可用为止,它返回的值不为空时说明memoryDetector 软引用对象被GC掉了。
- 调用new MemoryMonitorService().start()启动监控线程。一般来说,上面代码里的doPartClean()工作是由专门的清理类来辅助的。