其他示例

前面介绍了最常见的GC性能问题。但我们学到的很多原理都没有具体的场景来展现。本节介绍一些不常发生, 但也可能会碰到的问题。

RMI 与 GC

如果系统提供或者消费 RMI 服务, 则JVM会定期执行 full GC 来确保本地未使用的对象在另一端也不占用空间. 记住, 即使你的代码中没有发布 RMI 服务, 但第三方或者工具库也可能会打开 RMI 终端. 最常见的元凶是 JMX, 如果通过JMX连接到远端, 底层则会使用 RMI 发布数据。

问题是有很多不必要的周期性 full GC。查看老年代的使用情况, 一般是没有内存压力, 其中还存在大量的空闲区域, 但 full GC 就是被触发了, 也就会暂停所有的应用线程。

这种周期性调用 System.gc() 删除远程引用的行为, 是在 sun.rmi.transport.ObjectTable 类中, 通过 sun.misc.GC.requestLatency(long gcInterval) 调用的。

对许多应用来说, 根本没必要, 甚至对性能有害。 禁止这种周期性的 GC 行为, 可以使用以下 JVM 参数:

  1. java -Dsun.rmi.dgc.server.gcInterval=9223372036854775807L
  2. -Dsun.rmi.dgc.client.gcInterval=9223372036854775807L
  3. com.yourcompany.YourApplication

这让 Long.MAX_VALUE 毫秒之后, 才调用 System.gc()), 实际运行的系统可能永远都不会触发。

ObjectTable.class

  1. private static final long gcInterval =
  2. ((Long)AccessController.doPrivileged(
  3. new GetLongAction("sun.rmi.dgc.server.gcInterval", 3600000L)
  4. )).longValue();

可以看到, 默认值为 3600000L,也就是1小时触发一次 Full GC。

另一种方式是指定JVM参数 -XX:+DisableExplicitGC, 禁止显式地调用 System.gc(). 但我们强烈反对 这种方式, 因为埋有地雷。

JVMTI tagging 与 GC

如果在程序启动时指定了 Java Agent (-javaagent), agent 就可以使用 JVMTI tagging 标记堆中的对象。agent 使用tagging的种种原因本手册不详细讲解, 但如果 tagging 标记了大量的对象, 很可能会引起 GC 性能问题, 导致延迟增加, 以及吞吐量降低。

问题发生在 native 代码中, JvmtiTagMap::do_weak_oops 在每次GC时, 都会遍历所有标签(tag),并执行一些比较耗时的操作。更坑的是, 这种操作是串行执行的。

如果存在大量的标签, 就意味着 GC 时有很大一部分工作是单线程执行的, GC暂停时间可能会增加一个数量级。

检查是否因为 agent 增加了GC暂停时间, 可以使用诊断参数 –XX:+TraceJVMTIObjectTagging. 启用跟踪之后, 可以估算出内存中 tag 映射了多少 native 内存, 以及遍历所消耗的时间。

如果你不是 agent 的作者, 那一般是搞不定这类问题的。除了提BUG之外你什么都做不了. 如果发生了这种情况, 请建议厂商清理不必要的标签。

巨无霸对象的分配(Humongous Allocations)

如果使用 G1 垃圾收集算法, 会产生一种巨无霸对象引起的 GC 性能问题。

说明: 在G1中, 巨无霸对象是指所占空间超过一个小堆区(region) 50% 的对象。

频繁的创建巨无霸对象, 无疑会造成GC的性能问题, 看看G1的处理方式:

  • 如果某个 region 中含有巨无霸对象, 则巨无霸对象后面的空间将不会被分配。如果所有巨无霸对象都超过某个比例, 则未使用的空间就会引发内存碎片问题。
  • G1 没有对巨无霸对象进行优化。这在 JDK 8 以前是个特别棘手的问题 —— 在 Java 1.8u40 之前的版本中, 巨无霸对象所在 region 的回收只能在 full GC 中进行。最新版本的 Hotspot JVM, 在 marking 阶段之后的 cleanup 阶段中释放巨无霸区间, 所以这个问题在新版本JVM中的影响已大大降低。

要监控是否存在巨无霸对象, 可以打开GC日志, 使用的命令如下:

  1. java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
  2. -XX:+PrintReferenceGC -XX:+UseG1GC
  3. -XX:+PrintAdaptiveSizePolicy -Xmx128m
  4. MyClass

GC 日志中可能会发现这样的部分:

  1. 0.106: [G1Ergonomics (Concurrent Cycles)
  2. request concurrent cycle initiation,
  3. reason: occupancy higher than threshold,
  4. occupancy: 60817408 bytes,
  5. allocation request: 1048592 bytes,
  6. threshold: 60397965 bytes (45.00 %),
  7. source: concurrent humongous allocation]
  8. 0.106: [G1Ergonomics (Concurrent Cycles)
  9. request concurrent cycle initiation,
  10. reason: requested by GC cause,
  11. GC cause: G1 Humongous Allocation]
  12. 0.106: [G1Ergonomics (Concurrent Cycles)
  13. initiate concurrent cycle,
  14. reason: concurrent cycle initiation requested]
  15. 0.106: [GC pause (G1 Humongous Allocation)
  16. (young) (initial-mark)
  17. 0.106: [G1Ergonomics (CSet Construction)
  18. start choosing CSet,
  19. _pending_cards: 0,
  20. predicted base
  21. time: 10.00 ms,
  22. remaining time: 190.00 ms,
  23. target pause time: 200.00 ms]

这样的日志就是证据, 表明程序中确实创建了巨无霸对象. 可以看到: G1 Humongous Allocation 是 GC暂停的原因。 再看前面一点的 allocation request: 1048592 bytes , 可以发现程序试图分配一个 1,048,592 字节的对象, 这要比巨无霸区域(2MB)的 50% 多出 16 个字节。

第一种解决方式, 是修改 region size , 以使得大多数的对象不超过 50%, 也就不进行巨无霸对象区域的分配。 region 的默认大小在启动时根据堆内存的大小算出。但也可以指定参数来覆盖默认设置, -XX:G1HeapRegionSize=XX。 指定的 region size 必须在 1~32MB 之间, 还必须是2的幂 【2^10 = 1024 = 1KB; 2^20=1MB; 所以 region size 只能是: 1m,2m,4m,8m,16m,32m】。

这种方式也有副作用, 增加 region 的大小也就变相地减少了 region 的数量, 所以需要谨慎使用, 最好进行一些测试, 看看是否改善了吞吐量和延迟。

更好的方式需要一些工作量, 如果可以的话, 在程序中限制对象的大小。最好是使用分析器, 展示出巨无霸对象的信息, 以及分配时所在的堆栈跟踪信息。

总结

JVM上运行的程序多种多样, 启动参数也有上百个, 其中有很多会影响到 GC, 所以调优GC性能的方法也有很多种。

还是那句话, 没有真正的银弹, 能满足所有的性能调优指标。 我们能做的只是介绍一些常见的/和不常见的示例, 让你在碰到类似问题时知道是怎么回事。深入理解GC的工作原理, 熟练应用各种工具, 就可以进行GC调优, 提高程序性能。

原文链接: GC Tuning: In Practice