其他示例
前面介绍了最常见的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 参数:
java -Dsun.rmi.dgc.server.gcInterval=9223372036854775807L
-Dsun.rmi.dgc.client.gcInterval=9223372036854775807L
com.yourcompany.YourApplication
这让 Long.MAX_VALUE
毫秒之后, 才调用 System.gc()
), 实际运行的系统可能永远都不会触发。
ObjectTable.class
private static final long gcInterval =
((Long)AccessController.doPrivileged(
new GetLongAction("sun.rmi.dgc.server.gcInterval", 3600000L)
)).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日志, 使用的命令如下:
java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-XX:+PrintReferenceGC -XX:+UseG1GC
-XX:+PrintAdaptiveSizePolicy -Xmx128m
MyClass
GC 日志中可能会发现这样的部分:
0.106: [G1Ergonomics (Concurrent Cycles)
request concurrent cycle initiation,
reason: occupancy higher than threshold,
occupancy: 60817408 bytes,
allocation request: 1048592 bytes,
threshold: 60397965 bytes (45.00 %),
source: concurrent humongous allocation]
0.106: [G1Ergonomics (Concurrent Cycles)
request concurrent cycle initiation,
reason: requested by GC cause,
GC cause: G1 Humongous Allocation]
0.106: [G1Ergonomics (Concurrent Cycles)
initiate concurrent cycle,
reason: concurrent cycle initiation requested]
0.106: [GC pause (G1 Humongous Allocation)
(young) (initial-mark)
0.106: [G1Ergonomics (CSet Construction)
start choosing CSet,
_pending_cards: 0,
predicted base
time: 10.00 ms,
remaining time: 190.00 ms,
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