掌握 JVM 垃圾收集线程:简化 VM 选项

垃圾收集阶段对于任何 Java 应用程序都至关重要。主要目标是保持高吞吐量和低延迟之间的平衡。通过配置垃圾收集器,我们可以提高性能,或者至少推动应用程序朝着特定的方向发展。

垃圾收集周期越短越好。因此,分配给垃圾收集器的资源越多,它的工作速度就越快,从而整体上改善我们的应用程序。为垃圾收集器分配更多资源是一个合理的想法,但它并不像听起来那么简单。

在本文中,我们将了解线程数对应用程序性能的影响。本文主要介绍直接在主机上运行的应用程序。**但是,我们也可以将这些见解应用于容器化应用程序。 **

垃圾收集线程

通常,垃圾收集器会使用多个线程来促进正确及时的收集过程。然而,我们通常对与之连接的两种类型的线程感兴趣:并行线程和并发线程。

请注意,某些算法可能会出于不同目的使用更多线程。 例如,G1 垃圾收集器会使用更多线程执行某些内部任务。

1.并发线程

这些线程的名称是不言而喻的。 垃圾收集器在并发阶段使用并发线程。 例如,CMS 将使用它们来清理旧代。提供更多线程有助于减少应用程序的延迟。

同时,完全 STW(stop-the-world)垃圾收集器不会使用它们,因为它们没有并发阶段。

2. 并行线程

这些线程在 STW 阶段发挥作用。 尽管名称如此,但它们并不与应用程序并行运行。这些线程将彼此并行运行,以减少垃圾收集暂停。

因此,更多的并行线程会减少 STW 时间。但是,我们需要验证所有假设并运行适当的基准测试。JVM使用合理的启发式方法根据 CPU 核心数来确定默认的垃圾收集线程数。这个想法很简单:更多核心等于更多线程:

在具有 N 个硬件线程且 N 大于 8 的机器上,并行收集器使用 N 的固定分数作为垃圾收集器线程数。对于较大的 N 值,该分数约为 5/8。当 N 值低于 8 时,使用的数字为 N。在选定的平台上,该分数下降到 5/16。

因此,在八核机器上,我们将有八个收集器线程;在三十二核的机器上,这个数字将是二十个。

手动配置

虽然 JVM 提供了有关默认线程数的合理启发式方法,但有时我们想要覆盖它们。 我们可以使用 VM 选项手动配置线程数。 显式配置可以更好地控制应用程序,并允许我们使用更合适的值覆盖默认值。

我们可以使用几个标志来配置线程数。正如我们所讨论的,垃圾收集过程通常使用两种类型的线程:并发和并行;我们有单独的参数来控制它们:

-XX:ParallelGCThreads=<number>
-XX:ConcGCThreads=<threads>

手动配置的主要问题是我们应该深刻理解我们的应用程序: 不完全是领域逻辑和类之间的联系,而是影响堆的资源消耗和进程。

我们还必须了解 JVM 和应用程序使用的垃圾收集算法。理解不当可能会导致性能下降。**同时,向 SerialGC 添加更多并发线程不会对其产生任何影响。 **

线程数

让我们使用一个简单的应用程序来检查垃圾收集线程。但是,我们不会在空闲应用程序上看到所有线程。因此,我们需要对垃圾收集器施加压力,以便它尝试利用所有资源:

public class GcThreadsOnOutOfMemoryErrorBenchmark {
    public static void main(String[] args) {
        LinkedList<String> strings = new LinkedList<>();
        while (true) {
            strings.add(new String("Hello World!!!!"));
        }
    }
}

此代码将导致OutOfMemoryError并使应用程序崩溃。但是,这正是我们强制它使用所有可用的垃圾收集线程所需要的。要在**OutOfMemoryError时创建线程转储,我们可以使用以下命令:

-XX:OnOutOfMemoryError= "kill -3 %p"

我们把并发线程数改为2个,并行线程数改为8个,可以使用*-XX:+PrintCommandLineFlags*来验证一下参数:

-XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=268435456 -XX:MinHeapSize=6815736 -XX:OnOutOfMemoryError=kill -3 %p -XX:ParallelGCThreads=8 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC

根据之前的 VM 标志列表,我们将使用 G1GC 来查看并发和并行线程。 此外,我们还可以看到特定于此算法的其他垃圾收集线程:

Threads class SMR info:
_java_thread_list=0x0000600002d0ae80, length=12, elements={
0x0000000142016c00, 0x000000014380dc00, 0x000000014380b800, 0x00000001420d1600,
0x00000001420cf600, 0x000000014180b000, 0x00000001420d3400, 0x00000001420d6800,
0x000000014180d800, 0x0000000143009800, 0x0000000141812a00, 0x0000000141813000
}

"main" #1 [8451] prio=5 os_prio=31 cpu=68.74ms elapsed=1.41s tid=0x0000000142016c00 nid=8451 runnable  [0x000000016d7d6000]
   java.lang.Thread.State: RUNNABLE

"Reference Handler" #8 [31747] daemon prio=10 os_prio=31 cpu=0.43ms elapsed=1.40s tid=0x000000014380dc00 nid=31747 runnable  [0x000000016e742000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #9 [22531] daemon prio=8 os_prio=31 cpu=0.14ms elapsed=1.40s tid=0x000000014380b800 nid=22531 in Object.wait()  [0x000000016e94e000]
   java.lang.Thread.State: WAITING (on object monitor)

"Signal Dispatcher" #10 [31235] daemon prio=9 os_prio=31 cpu=0.07ms elapsed=1.40s tid=0x00000001420d1600 nid=31235 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Service Thread" #11 [23043] daemon prio=9 os_prio=31 cpu=0.15ms elapsed=1.40s tid=0x00000001420cf600 nid=23043 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Deflation Thread" #12 [30723] daemon prio=9 os_prio=31 cpu=0.03ms elapsed=1.40s tid=0x000000014180b000 nid=30723 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #13 [23555] daemon prio=9 os_prio=31 cpu=18.32ms elapsed=1.40s tid=0x00000001420d3400 nid=23555 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

"C1 CompilerThread0" #16 [24067] daemon prio=9 os_prio=31 cpu=18.20ms elapsed=1.40s tid=0x00000001420d6800 nid=24067 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

"Sweeper thread" #17 [30211] daemon prio=9 os_prio=31 cpu=0.04ms elapsed=1.40s tid=0x000000014180d800 nid=30211 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Common-Cleaner" #18 [29699] daemon prio=8 os_prio=31 cpu=0.20ms elapsed=1.40s tid=0x0000000143009800 nid=29699 waiting on condition  [0x000000016f7a2000]
   java.lang.Thread.State: TIMED_WAITING (parking)

"Monitor Ctrl-Break" #19 [24835] daemon prio=5 os_prio=31 cpu=14.32ms elapsed=1.37s tid=0x0000000141812a00 nid=24835 runnable  [0x000000016f9ae000]
   java.lang.Thread.State: RUNNABLE

"Notification Thread" #20 [25347] daemon prio=9 os_prio=31 cpu=0.04ms elapsed=1.37s tid=0x0000000141813000 nid=25347 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"VM Thread" os_prio=31 cpu=16.53ms elapsed=1.41s tid=0x000000014170e060 nid=19971 runnable  

"GC Thread#0" os_prio=31 cpu=619.54ms elapsed=1.41s tid=0x0000000141706da0 nid=12547 runnable  

"GC Thread#1" os_prio=31 cpu=672.72ms elapsed=1.36s tid=0x000000014161b6d0 nid=28931 runnable  

"GC Thread#2" os_prio=31 cpu=559.43ms elapsed=1.36s tid=0x000000014161bb70 nid=25859 runnable  

"GC Thread#3" os_prio=31 cpu=914.80ms elapsed=1.36s tid=0x000000014161c010 nid=26115 runnable  

"GC Thread#4" os_prio=31 cpu=613.24ms elapsed=1.36s tid=0x000000014161c4b0 nid=28163 runnable  

"GC Thread#5" os_prio=31 cpu=491.98ms elapsed=1.36s tid=0x000000014161c950 nid=27907 runnable  

"GC Thread#6" os_prio=31 cpu=844.52ms elapsed=1.36s tid=0x000000014161cdf0 nid=27395 runnable  

"GC Thread#7" os_prio=31 cpu=662.01ms elapsed=1.36s tid=0x000000014161d290 nid=27139 runnable  

"G1 Main Marker" os_prio=31 cpu=0.13ms elapsed=1.41s tid=0x0000000141707470 nid=14339 runnable  

"G1 Conc#0" os_prio=31 cpu=17.27ms elapsed=1.41s tid=0x0000000141707d10 nid=13827 runnable  

"G1 Conc#1" os_prio=31 cpu=17.35ms elapsed=1.09s tid=0x000000014161dc30 nid=43267 runnable  

"G1 Refine#0" os_prio=31 cpu=1.00ms elapsed=1.41s tid=0x0000000141709ba0 nid=16643 runnable  

"G1 Service" os_prio=31 cpu=0.41ms elapsed=1.41s tid=0x000000014170a4d0 nid=21507 runnable  

"VM Periodic Task Thread" os_prio=31 cpu=0.22ms elapsed=1.37s tid=0x000000014161a930 nid=25603 waiting on condition

虽然手动配置可能很有用,但也可能导致严重问题。 我们可以将自动计算与限制相结合来避免错误。 以下标志可以帮助我们自动计算线程数,但也允许我们设置一些限制:

-XX:+AdaptiveGCThreading
-XX:ParallelGCMaxThreads

线程转储分析

我们可以在应用程序中创建线程转储,因为它们不会产生太多开销。**最好多次创建线程转储,每次之间都留出短暂的停顿,例如 10 秒。 **

虽然线程转储通常可读,但更方便的是可视化和比较随时间变化的信息。 标准工具可以帮助我们可视化、过滤和搜索,但它们没有提供比较转储的便捷方法

yCrash 提供了一些工具,可以帮助分析和测试应用程序的性能。其中 之一是fastThread 它可以根据线程转储创建综合报告:

fastThread 报告摘要
如图:快速线程报告 摘要

然后可以对这些报告进行比较和审查:

在这里插入图片描述

如图:快速线程比较总结

可以通过 yCrash 仪表板访问这些报告,以访问同一应用程序中的所有报告。

另外,我们可以在同一个报告页面上检查线程池:

fastThread 线程池
如图:快速线程线程池

结论

垃圾收集线程的默认配置通常都很好。 但是,JVM 允许手动配置,我们可以使用它来微调应用程序并提高其性能。此过程应包括监控和分析。

线程转储不会产生太多开销 ,因此我们可以定期运行它们。 这样,我们可以在不同阶段监控应用程序的变化,这有助于识别热点。

然而,为了更好地了解应用程序的问题,必须进行几次线程转储,并在每次转储之间进行短暂的暂停。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

国通快递驿站

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值