示例

让我们看一个弱引用示例, 其中创建了大量的对象, 并在 minor GC 中完成回收。和前面一样, 修改提升阀值。使用的JVM参数为: -Xmx24m -XX:NewSize=16m -XX:MaxTenuringThreshold=1 , GC日志如下所示:

  1. 2.330: [GC (Allocation Failure) 20933K->8229K(22528K), 0.0033848 secs]
  2. 2.335: [GC (Allocation Failure) 20517K->7813K(22528K), 0.0022426 secs]
  3. 2.339: [GC (Allocation Failure) 20101K->7429K(22528K), 0.0010920 secs]
  4. 2.341: [GC (Allocation Failure) 19717K->9157K(22528K), 0.0056285 secs]
  5. 2.348: [GC (Allocation Failure) 21445K->8997K(22528K), 0.0041313 secs]
  6. 2.354: [GC (Allocation Failure) 21285K->8581K(22528K), 0.0033737 secs]
  7. 2.359: [GC (Allocation Failure) 20869K->8197K(22528K), 0.0023407 secs]
  8. 2.362: [GC (Allocation Failure) 20485K->7845K(22528K), 0.0011553 secs]
  9. 2.365: [GC (Allocation Failure) 20133K->9501K(22528K), 0.0060705 secs]
  10. 2.371: [Full GC (Ergonomics) 9501K->2987K(22528K), 0.0171452 secs]

可以看到, Full GC 的次数很少。但如果使用弱引用来指向创建的对象, 使用JVM参数 -Dweak.refs=true, 则情况会发生明显变化. 使用弱引用的原因很多, 比如在 weak hash map 中将对象作为Key的情况。在任何情况下, 使用弱引用都可能会导致以下情形:

  1. 2.059: [Full GC (Ergonomics) 20365K->19611K(22528K), 0.0654090 secs]
  2. 2.125: [Full GC (Ergonomics) 20365K->19711K(22528K), 0.0707499 secs]
  3. 2.196: [Full GC (Ergonomics) 20365K->19798K(22528K), 0.0717052 secs]
  4. 2.268: [Full GC (Ergonomics) 20365K->19873K(22528K), 0.0686290 secs]
  5. 2.337: [Full GC (Ergonomics) 20365K->19939K(22528K), 0.0702009 secs]
  6. 2.407: [Full GC (Ergonomics) 20365K->19995K(22528K), 0.0694095 secs]

可以看到, 发生了多次 full GC, 比起前一节的示例, GC时间增加了一个数量级! 这是过早提升的另一个例子, 但这次情况更加棘手. 当然,问题的根源在于弱引用。这些临死的对象, 在添加弱引用之后, 被提升到了老年代。 但是, 他们现在陷入另一次GC循环之中, 所以需要对其做一些适当的清理。像之前一样, 最简单的办法是增加年轻代的大小, 例如指定JVM参数: -Xmx64m -XX:NewSize=32m:

  1. 2.328: [GC (Allocation Failure) 38940K->13596K(61440K), 0.0012818 secs]
  2. 2.332: [GC (Allocation Failure) 38172K->14812K(61440K), 0.0060333 secs]
  3. 2.341: [GC (Allocation Failure) 39388K->13948K(61440K), 0.0029427 secs]
  4. 2.347: [GC (Allocation Failure) 38524K->15228K(61440K), 0.0101199 secs]
  5. 2.361: [GC (Allocation Failure) 39804K->14428K(61440K), 0.0040940 secs]
  6. 2.368: [GC (Allocation Failure) 39004K->13532K(61440K), 0.0012451 secs]

这时候, 对象在 minor GC 中就被回收了。

更坏的情况是使用软引用,例如这个软引用示例程序。如果程序不是即将发生 OutOfMemoryError , 软引用对象就不会被回收. 在示例程序中,用软引用替代弱引用, 立即出现了更多的 Full GC 事件:

  1. 2.162: [Full GC (Ergonomics) 31561K->12865K(61440K), 0.0181392 secs]
  2. 2.184: [GC (Allocation Failure) 37441K->17585K(61440K), 0.0024479 secs]
  3. 2.189: [GC (Allocation Failure) 42161K->27033K(61440K), 0.0061485 secs]
  4. 2.195: [Full GC (Ergonomics) 27033K->14385K(61440K), 0.0228773 secs]
  5. 2.221: [GC (Allocation Failure) 38961K->20633K(61440K), 0.0030729 secs]
  6. 2.227: [GC (Allocation Failure) 45209K->31609K(61440K), 0.0069772 secs]
  7. 2.234: [Full GC (Ergonomics) 31609K->15905K(61440K), 0.0257689 secs]

最有趣的是虚引用示例中的虚引用, 使用同样的JVM参数启动, 其结果和弱引用示例非常相似。实际上, full GC 暂停的次数会小得多, 原因前面说过, 他们有不同的终结方式。

如果禁用虚引用清理, 增加JVM启动参数 (-Dno.ref.clearing=true), 则可以看到:

  1. 4.180: [Full GC (Ergonomics) 57343K->57087K(61440K), 0.0879851 secs]
  2. 4.269: [Full GC (Ergonomics) 57089K->57088K(61440K), 0.0973912 secs]
  3. 4.366: [Full GC (Ergonomics) 57091K->57089K(61440K), 0.0948099 secs]

main 线程中抛出异常 java.lang.OutOfMemoryError: Java heap space.

使用虚引用时要小心谨慎, 并及时清理虚可达对象。如果不清理, 很可能会发生 OutOfMemoryError. 请相信我们的经验教训: 处理 reference queue 的线程中如果没 catch 住 exception , 系统很快就会被整挂了。

使用非强引用的影响

建议使用JVM参数 -XX:+PrintReferenceGC 来看看各种引用对GC的影响. 如果将此参数用于启动 弱引用示例 , 将会看到:

  1. 2.173: [Full GC (Ergonomics)
  2. 2.234: [SoftReference, 0 refs, 0.0000151 secs]
  3. 2.234: [WeakReference, 2648 refs, 0.0001714 secs]
  4. 2.234: [FinalReference, 1 refs, 0.0000037 secs]
  5. 2.234: [PhantomReference, 0 refs, 0 refs, 0.0000039 secs]
  6. 2.234: [JNI Weak Reference, 0.0000027 secs]
  7. [PSYoungGen: 9216K->8676K(10752K)]
  8. [ParOldGen: 12115K->12115K(12288K)]
  9. 21331K->20792K(23040K),
  10. [Metaspace: 3725K->3725K(1056768K)],
  11. 0.0766685 secs]
  12. [Times: user=0.49 sys=0.01, real=0.08 secs]
  13. 2.250: [Full GC (Ergonomics)
  14. 2.307: [SoftReference, 0 refs, 0.0000173 secs]
  15. 2.307: [WeakReference, 2298 refs, 0.0001535 secs]
  16. 2.307: [FinalReference, 3 refs, 0.0000043 secs]
  17. 2.307: [PhantomReference, 0 refs, 0 refs, 0.0000042 secs]
  18. 2.307: [JNI Weak Reference, 0.0000029 secs]
  19. [PSYoungGen: 9215K->8747K(10752K)]
  20. [ParOldGen: 12115K->12115K(12288K)]
  21. 21331K->20863K(23040K),
  22. [Metaspace: 3725K->3725K(1056768K)],
  23. 0.0734832 secs]
  24. [Times: user=0.52 sys=0.01, real=0.07 secs]
  25. 2.323: [Full GC (Ergonomics)
  26. 2.383: [SoftReference, 0 refs, 0.0000161 secs]
  27. 2.383: [WeakReference, 1981 refs, 0.0001292 secs]
  28. 2.383: [FinalReference, 16 refs, 0.0000049 secs]
  29. 2.383: [PhantomReference, 0 refs, 0 refs, 0.0000040 secs]
  30. 2.383: [JNI Weak Reference, 0.0000027 secs]
  31. [PSYoungGen: 9216K->8809K(10752K)]
  32. [ParOldGen: 12115K->12115K(12288K)]
  33. 21331K->20925K(23040K),
  34. [Metaspace: 3725K->3725K(1056768K)],
  35. 0.0738414 secs]
  36. [Times: user=0.52 sys=0.01, real=0.08 secs]

只有确定 GC 对应用的吞吐量和延迟造成影响之后, 才应该花心思来分析这些信息, 审查这部分日志。通常情况下, 每次GC清理的引用数量都是很少的, 大部分情况下为 0。如果GC 花了较多时间来清理这类引用, 或者清除了很多的此类引用, 就需要进一步观察和分析了。

解决方案

如果程序确实碰到了 mis-, ab- 问题或者滥用 weak, soft, phantom 引用, 一般都要修改程序的实现逻辑。每个系统不一样, 因此很难提供通用的指导建议, 但有一些常用的办法:

  • 弱引用(Weak references) —— 如果某个内存池的使用量增大, 造成了性能问题, 那么增加这个内存池的大小(可能也要增加堆内存的最大容量)。如同示例中所看到的, 增加堆内存的大小, 以及年轻代的大小, 可以减轻症状。
  • 虚引用(Phantom references) —— 请确保在程序中调用了虚引用的 clear 方法。编程中很容易忽略某些虚引用, 或者清理的速度跟不上生产的速度, 又或者清除引用队列的线程挂了, 就会对GC 造成很大压力, 最终可能引起 OutOfMemoryError
  • 软引用(Soft references) —— 如果确定问题的根源是软引用, 唯一的解决办法是修改程序源码, 改变内部实现逻辑。