影响GC的另一类问题是非强引用(软弱虚引用对象)的使用有关,虽然在许多情况下可能有助于避免不必要的OutOfMemoryError,但是大量使用这样的引用会影响垃圾收集的行为方式,影响应用程序的性能。
在使用弱引用时,应该知道弱引用进行垃圾收集的方式。每当GC发现一个对象是弱可达对象时(也就是说对该对象的最后一个引用是弱引用),该弱可达对象就会放到相应的ReferenceQueue上成为符合结束条件的对象。然后轮询这个引用队列并执行相关的清理活动。这种清除的一个典型例子是从缓存中删除现在丢失的键。
解决方式是,你仍然可以创建新的强引用对原来的对象。因此在最终完成并回收该对象之前,GC必须再次检查是否真的可以这样做。这样非强引用就不会在额外的GC周期中回收。
弱引用实际上比想想的常见的多。许多缓存解决方案使用弱引用构建实现,所以即使你没有在代码中直接创建任何对象,应用程序当中仍然有很大的肯能存在大量的非强引用对象。
要记住:在使用软引用对象时,软引用的收集急切程度远低于弱引用,收集软引用的时间点并没有指定,而是取决于JVM的实现。通常,软引用的收集仅作为内存耗尽的最后一搏。这意味着存在软引用收集将面临着Full GC的频次更多、时间更长(因为在Old Generation存有更多的对象)。
虚引用对象:使用虚引用对象必须手动进行内存管理(将这些虚引用对象标记为可回收的引用对象)。如果不手动标记的话会造成很大的麻烦。
为了确保可回收对象保持原样,可能不会检索虚引用的引用对象,虚引用的get方法总是返回null;
大部分开发人员忽略了javadoc中的一段内容:
与软引用和弱引用不同,虚引用进入队列时不会被垃圾收集器自动清理。通过虚应用可达的对象保持直到他们变得不可达或所有这样的引用被清除。
也就是说,我们必须手动调用清除虚引用的应用对象,否则就会面临OutOfMemoryError的风险。虚引用存在的原因首先是允许通过通常的方法知道一个对象什么时候变得实际不可达。不像软引用对象和虚引用对象你不能复活一个虚引用对象。
下面是一个示例,该应用程序分配了许多对象,这些对象在Minor GC执行时成功回收。修改租用阈值改变提升率,我们可以指定JVM启动参数-Xmx24m -XX:NewSize=16m -XX:MaxTenuringThreshold=1,GC log如下所示:
在上面指定的JVM参数的基础上执行应用程序,Full GC频率较低。但是如果JVM加上参数(-Dweak.refs=true)开启创建这些创建对象的弱引用,情况可能发生很大的变化。开启创建对象的弱引用的原因有很多,从使用对象做弱HashMap的key开始到对象空间分配配置结束。
在任何情况下使用弱引用会导致以下问题:
正如上面的log所示,现在有许多Full GC,并且比没设置(-Dweak.refs=true)开启弱引用时的Full GC时间多了一个数量级。又是一个过早提升的例子,但是这次难度比较大,当然根本原因是使用到了开启了弱引用,在没有开启弱引用的时候,应用程序创建的对象在Young Generation 进行Minor GC的时候已经被收集,不会进入到Old Generation。但是开启弱引用之后,原本会被Minor GC收集的对象会经理多次Minor GC并提升到了Old Generation以便进行适当的清理。像以前一样一个简单的解决方案是通过指定JVM参数(-Xmx64m -XX:NewSize=32m)来增加年轻代的大小。
如上所示(-Xmx64m -XX:NewSize=32m)指定增大了Old Generation 的空间大小之后又开始在Minor GC进行了回收。
如果在下一个应用程序当中使用软引用,情况会更糟,直到应用程序面临OutOfMemoryError的时候,软引用可达对象才会被回收,在
下面的引用程序gc日志中可以看出,用软引用替换弱引用会立即出现更多的Full GC。
这里重中之重是第三个应用程序中看到的虚引用。使用之前相同的参数运行应用程序情况下与弱引用下的结果非常相似。事实上Full GC的数量要小得多,因为终止化方式不同。
添加(-Dno.ref.clearing=true)禁止虚引用的参数,很快会给我们这样的结果:
使用虚引用时,必须非常谨慎,需要及时地虚引用可达的对象。如果不这样就会导致OutOfMemoryError错误。在处理引用队列的线程中出现的意外异常,会导致应用程序无法使用。
JVM受到了怎样的影响?
指定JVM参数-XX:+PrintReferenceGC查看不同引用类型对象对GC的影响。如果我们将此参数添加到WeakReference应用程序的例子中,会得到下面的log:
像以前一样,只有分析GC的吞吐量和延迟时间才能看出各种引用类型对象对GC的影响。通常情况下GC对非强引用的处理非常少,在许多情况下是0。但是如果引用程序花费大量的时间清除引用或大量的非强引用被清理,那么就需要进一步的分析。
解决方案有哪些?
当你的应用程序实际上过度地使用了(软、弱、虚引用),通常是是更改引用程序的内在逻辑,没有通用的方式,只能根据实际的业务逻辑去修改。另外还有一些通用的解决方案:
- 弱引用:如果是由特定的内存池对象增加引起的,那么增加这个特定的内存池(可能包括整个heap),可以帮助你解决问题。如上面的示例部分所示增加heap和Young Generation的大小可以缓解这种问题。
- 虚引用:确保你使用过虚引用对象之后手动清理这些虚引用,很容易忽略某些角落里的虚引用、清理线程无法跟上虚引用对象填充队列的速度,这样会给GC造成很大的压力,肯能到最后造成OutOfMemoryError。
- 软引用:当软引用造成JVM出现问题时,解决问题的唯一方式是更改应用程序的内在逻辑。
参考《Plumbr Handbook Java 》