广告占位

译文: 垃圾收集简介

胡建洪   2019-02-16 阅读(88) 评论(1) 点赞(6)

摘要:垃圾收集是Java虚拟机的杀手级功能之一。使开发人员不必担心内存管理(如C ++等语言),可以显着提高生产力。但是,这种提升并不是免费的:垃圾收集需要付出代价。

垃圾收集是Java虚拟机的杀手级功能之一。使开发人员不必担心内存管理(如C ++等语言),可以显着提高生产力。但是,这种提升并不是免费的:垃圾收集需要付出代价。

当我们在程序中分配和取消分配对象时,垃圾收集器负责执行清理工作,以释放不再需要的对象占用的内存,以便可以将其用于新对象。收集器的处理方式取决于我们选择的收集算法。

有关垃圾收集特性需要考虑两个主要向量 - 吞吐量和暂停。吞吐量是未在垃圾收集中花费的总时间的百分比,而暂停是应用程序因为正在执行垃圾收集而显示无响应的时间。

根据应用程序的类型,我们可能更关心其中一个,但在大多数实际应用程序中,暂停是常见问题。堆中的JVM使用的内存越多,运行垃圾收集(在年轻代或终身代)中就会发生暂停的可能性越大。如今,大多数团队最终将JVM内存使用量限制在4-8GB,这是因为否则可能会出现长时间的暂停。因此,我们必须能够衡量我们的应用程序和垃圾收集的行为。更具体地说,重要的是我们有关于实际生产应用程序运行的信息。

为了测量垃圾收集,有三种主要方法:

  • 分析JVM打印的垃圾收集日志;
  • 使用JMX读取暴露的垃圾收集信息;
  • 使用jstat等工具访问JVM检测计数器。

我们将单独关注这些方法中的每一种,并了解如何使用它们来测量GC行为。

分析垃圾收集日志

您需要做的第一件事是在java调用中添加参数以打印出垃圾收集信息:

  • -Xloggc:〜/ gc.log
  • -XX:+ PrintGCDetails
  • -XX:+ PrintGCTimeStamps
  • -XX:+ PrintTenuringDistribution

示例输出(忽略期限分布)将是:

0.634: [GC  [PSYoungGen: 10304K->1660K(11968K)] 10304K->2656K(39296K), 0.0069210 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

这告诉我们的是,在年轻一代的流程开始后0.634秒发生了一次小集合。在收集之前,年轻一代中有10304K物体,之后在同一代中有1660K(年轻人的总体大小为11968K),花了~7毫秒。

下一个数字指的是堆的总大小 - 总堆使用量为10304K(意味着我们之前只有年轻的gen堆使用),减少到2656K(大于年轻的1660K,这意味着我们推广996K到老一代)。堆(年轻+老一代)的总大小现在是39296K。

完整gc的示例如下所示:

4.268: [Full GC [PSYoungGen: 1649K->0K(42880K)] [ParOldGen: 19757K->18765K(44928K)] 21406K->18765K(87808K) [PSPermGen: 23549K->23537K(47104K)], 0.2047590 secs] [Times: user=0.31 sys=0.00, real=0.20 secs]

这个输出的结构和以前一样,但是在这里我们可以看到堆的三个不同区域 - 年轻,老一代和永久生成 - 以及它们的大小如何变化。

在调整JVM的生成大小和不同参数时,这些数据肯定是有价值的,但对于我们监控生产中的垃圾收集行为的目标也非常有用。

在生产中编写这些日志的影响可以忽略不计,因此解析它们是切合实际的,并在必要时发送警报 - 例如,向团队发送电子邮件或通知公司的运营警报系统。

一个简单的例子是监视此文件并在发生完整垃圾收集时发送电子邮件。这是在Unix系统中实现的一种方式:

tail -f gc.log | grep –line-buffered “Full GC” | while read line; do echo “$line” | mail -s “Full garbage collection occurred” “email@domain.com”; done

这只会将各个旧的收集行发送到电子邮件。它非常简单,但并不总是需要一个复杂且完全成熟的解决方案来满足您的需求。

有一些开源项目可以解析垃圾收集日志,但这些格式有两个问题:它们根据使用的收集器而有所不同,并且可能会随着时间而变化(过去也是这样做的!)。请注意,其中一些项目不再适用于当前的日志格式。

通过JMX读取垃圾收集信息

另一种读取垃圾收集信息的方法是通过Java Management Extensions(JMX)。

通过MBean公开的数据点有很多,可以在进程内部,同一台机器上的另一个进程或远程进程中读取。对于我们的具体情况,我们可以使用垃圾收集bean(这些也取决于我们使用的垃圾收集算法):

图1:使用MBean公开垃圾收集bean。

在jconsole中我们可以看到,除了其他信息,我们还有2个与GC相关的MBean:PS MarkSweep和PS Scavenge(在这种情况下,我们使用Concurrent Mark Sweep作为我们的垃圾收集算法)。

然而,我们可以远远超过简单地在jconsole或类似的管理GUI中查看这些数据。我们可以从代码中读取这些信息,并处理我们认为合适的情况。

清单1显示了我们如何阅读它。

public void monitor(int vmpid) throws Exception {
JMXServiceURL url = new JMXServiceURL(getVMAddress(vmpid));
        JMXConnector connector = JMXConnectorFactory.connect(url);

        final MBeanServerConnection serverConnection = connector.getMBeanServerConnection();
        Set<ObjectName> beanSet = serverConnection.queryNames(new ObjectName("java.lang:type=GarbageCollector,name=PS MarkSweep"), null);

        final ObjectName bean = beanSet.iterator().next();

        GarbageCollectorMXBean gcBean = JMX.newMXBeanProxy(serverConnection,bean, GarbageCollectorMXBean.class);
        System.out.println("collection time: " + gcBean.getCollectionTime());
        System.out.println("collection count: " + gcBean.getCollectionCount());

        GcInfo gcInfo = gcBean.getLastGcInfo();
        Map<String, MemoryUsage> memUsages = gcInfo.getMemoryUsageBeforeGc();
        for (Entry<String, MemoryUsage> memUsage : memUsages.entrySet()) {
                System.out.println(memUsage.getKey() + ": " + memUsage.getValue());
        }

        listenToNotifications(serverConnection, bean);
}

private String getVMAddress(int pid) throws AttachNotSupportedException, IOException {
        String jmxAddressProp = "com.sun.management.jmxremote.localConnectorAddress";
        VirtualMachine vm = VirtualMachine.attach(String.valueOf(pid));
        return vm.getAgentProperties().getProperty(jmxAddressProp);
}

清单2显示了一个示例输出。

`collection time: ``82037````collection count: ``116````PS Survivor Space: init = ``1703936``(1664K) used = ``65536``(64K) committed = ``32047104``(31296K) max = ``32047104``(31296K)``PS Eden Space: init = ``10551296``(10304K) used = ``0``(0K) committed = ``69795840``(68160K) max = ``113049600``(110400K)``PS Old Gen: init = ``27983872``(27328K) used = ``239432344``(233820K) committed = ``357957632``(349568K) max = ``357957632``(349568K)``Code Cache: init = ``2555904``(2496K) used = ``19949568``(19482K) committed = ``20185088``(19712K) max = ``50331648``(49152K)``PS Perm Gen: init = ``21757952``(21248K) used = ``148450536``(144971K) committed = ``155058176``(151424K) max = ``268435456``(262144K)`

我们只是打印出不同空间的收集时间,计数和统计数据,就像我们在查看垃圾收集日志时看到的那样。但是,在这种情况下,我们拥有丰富的数据结构,使我们可以在不需要解析文本的情况下获得此信息。

在这个简单的例子中,我们正在从本地机器上运行的进程中读取垃圾收集信息。为了做到这一点,我们需要的只是进程进程id,它被传递给monitor()方法。

您可以看到我们传递给queryNames()方法的标识符与我们在前面的jconsole屏幕截图中看到的树匹配:

`“java.lang:type=GarbageCollector,name=PS MarkSweep”`

我们可以在这里阅读PS Scavenge收集器(用于年轻代集合),您可能会更改此代码以便能够读取您决定配置的任何垃圾收集器。

在最后一行,我们正在调用listenToNotifications方法。这个方法看起来像清单3

private void listenToNotifications(final MBeanServerConnection serverConnection, final ObjectName bean) throws InstanceNotFoundException, IOException {
        final Queue<Notification> notifications = new LinkedBlockingQueue<Notification>();
        NotificationListener listener = new NotificationListener() {
                @Override
                public void handleNotification(Notification notification, Object ctx) {
                        notifications.add(notification);
                }
        };
        serverConnection.addNotificationListener(bean, listener, null, null);

        while (true) {
                Notification notification = notifications.poll();
                if (notification != null) {
                        process(notification);
                }
        }
}

此方法允许我们监听特定垃圾收集器中的更改 - 即每次收集发生时都会收到通知。流程方法未实现,在这种情况下,它将收集所需的任何信息并通过电子邮件发送,在内部警报系统中发布通知等。

访问JVM检测计数器

在生产中访问垃圾收集信息的最后但有趣的方法是访问实际的JVM计数器。当JVM运行时,它使用内部数据结构来保存有关垃圾收集的所有信息(以及更多)。这是直接在C ++中完成的,我们无法直接从Java访问它。

但是,JVM将这些数据结构存储在一个文件中,该文件通常保存在名为hsperfdata_ 的临时文件夹(Linux中的/ tmp)中,该文件夹包含所有信息。JVM将其作为内存映射文件进行写入和读取,因此可以通过简单地读取文件来访问这些数据结构。此方法的优点是它几乎不会影响正在运行的进程,因为文件是以只读模式打开的,以保证我们不会影响内存。这个过程没有额外的工作要做,这与JMX示例不同,在JMX示例中,代码需要执行才能访问我们想要的信息。

虽然实际完成此任务并不像前面的示例那么简单,但我们可以从现有工具中获得一些好的指示。与JDK一起分发的一个有趣的工具是jstat。在实践中做的是读取hsperfdata文件并在stdout上显示性能计数器。还有另一个名为jstatd的类似工具,一个允许远程进程(例如VisualVM)连接和查询数据的守护进程。但是,此通信的格式未公布,不保证保持不变。

有两种方法可以利用这种访问方法:使用jstat并像处理垃圾收集日志一样解析其输出,或者直接读取性能计数器数据结构。第二种选择对于本文的范围来说太复杂了,但jstat使用起来相当简单。我们可以像这样运行它:

`> jstat –gccause –t <pid> <millis_interval>`

这将获得每millis_interval的垃圾收集信息,并打印出与清单4类似的东西

`Timestamp S0 S1 E O P YGC YGCT FGC FGCT GCT LGCC GCC````82214.9` `9.04` `0.00` `78.21` `68.87` `96.27` `968` `20.731` `116` `82.039` `102.769` `Allocation Failure No GC````82215.9` `9.04` `0.00` `78.21` `68.87` `96.27` `968` `20.731` `116` `82.039` `102.769` `Allocation Failure No GC`

这为您提供了大部分基本计数器-survivor空间0,1,eden,旧的和永久的利用率百分比,垃圾收集计数,垃圾收集所花费的时间以及垃圾收集的原因。jstat的手册页对每个计数器以及可以传递给不同信息的其他选项有一个很好的解释。

与垃圾收集日志一样,我们可以以非常天真的方式解析此输出以发送电子邮件 - 但我会留给您解决。

结论

有许多方法可以用不同的努力和优势来监视生产中的垃圾收集行为。重要的是,这种行为在投入生产之前得到了测量,我们将这些监控解决方案投入生产,以获得有关我们运行系统的重要信息。

原文链接:https://jaxenter.com/an-introduction-to-garbage-collection-106154.html[](https://jaxenter.com/an-introduction-to-garbage-collection-106154.html)

喜欢 (6) or 分享 (0)

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请狠狠点击下面的

  Java GC
广告占位