Java中的大对象怎么了?


根据设计,G1垃圾收集器通过将堆划分为固定数量的相同大小的区域来管理堆。默认情况下,最大区域数为2048,并且区域大小与最大堆大小相对应,如下所示:堆大小<4GB:2MB,<8GB:4MB,<16GB:8MB,依此类推。通常,将对象分配到给定区域中,直到其满为止,然后在某个时候,GC通过从该区域撤离所有活动对象来释放整个区域。

但是,如果对象(通常是数组)大于区域大小的一半,则所有这些更改都会发生。此类对象在G1术语中称为Humongous,其处理方式如下:

庞大的对象直接在“旧世代”中分配(请注意,在JDK 11和更高版本中,可能会或可能不会)。 通过串联几个连续的区域,为每个这样的对象创建一个单独的巨大区域。其中一些区域可能需要先进行GC。 每个巨大的区域只能容纳一个巨大的物体,而不能容纳其他物体。也就是说,巨大对象的末端与巨大区域的末端之间的空间(在最坏的情况下可能接近正常区域大小的一半)(至少在JDK 8-11中是这样,但是可能会在最新的JDK版本中解决) 可以从Oracle网站上获得更多详细信息,但是从上面的描述中可以清楚地看到,从性能的角度来看,庞大的对象是不好的,因为

如果是在旧一代中分配的,则即使它们寿命短,也无法快速进行GC(旧一代的收集频率比年轻一代要低,并且花费更多时间) 从几个正常区域中创建一个巨大区域可能需要很短的时间 如果堆上有许多巨大的对象,则由于巨大区域中未使用的“间隙”,可能导致堆碎片。 根据Humongous对象的数量,大小和分配频率,后果可能会有所不同。相对温和的结果是增加了GC暂停时间。在最坏的情况下,OutOfMemoryError当使用的堆部分比整个堆小得多时,严重的堆碎片会导致JVM崩溃。这是一个简短的应用程序,可以说明此问题:

import java.util.ArrayList;
// This app runs until it crashes with OOM, to measure how many 1MB arrays
// can be allocated on the heap. For these arrays to be treated as Humongous,
// heap size (-Xmx) should be set to less than 4G.  
public class ExhaustHeap {
  private static final int ONE_MB = 1024 * 1024;
  public static void main(String args[]) {
    ArrayList<byte[]> list = new ArrayList<>(5000);
    for (int i = 0; i < 5000; i++) {
      // Note that a byte[] array with 1MB elements needs additional 16 bytes
      // of memory for its header. Thus it's bigger than half the 2MB region.
      list.add(new byte[ONE_MB]);
      System.out.println("Allocated arrays: " + i);
    }
  }
}

该应用程序将一直运行到耗尽堆并崩溃为止OutOfMemoryError。它打印的最后一行显示了它能够分配多少个1MB阵列。我在堆大小为3900MB的JDK 8和JDK 11上运行了此代码。使用并发标记扫描(CMS)GC(不会将堆划分成较小的区域)时,将分配3830个数组(即3830 MB)。但是,使用G1 GC时,分配的空间减少了两倍(1948个数组)。换句话说,使用CMS,几乎可以使用整个堆,而使用G1 GC,该应用程序实际上只能使用大约一半的堆!如果更改此代码以模拟更现实的情况(大小数组交替分配),结果将不会更好:该应用程序成功分配了大约2600MB的内存,但分配的数量却更少了-1310而不是1948 -个1MB阵列。

在分析OOM之后生成的堆转储时(如果-XX:+HeapDumpOnOutOfMemoryError启用了JVM标志),这种情况可能会引起混乱:这种转储可能比堆大小小得多。这是违反直觉的:如果转储很小,则意味着堆没有得到充分利用,即堆中有可用空间。但是,如果有可用空间,那么为什么JVM声称它已耗尽堆空间?答案是:是的,堆中有(很多)可用空间,但是新对象分配无法访问它。即使某些巨大的对象是垃圾,该GC也会进一步恶化,因为未知原因,GC可能(如我们多次观察)无法启动将释放这些对象并释放空间的Full GC。 。

诊断大量对象分配 最容易发现大量对象分配的地方是应用程序的GC日志。只是将其grep表示为“巨大”,您可能会看到类似以下内容:

2020-07-17T13:16:31.567+0000: 18663.443: Total time for which application threads were stopped: 0.0404361 seconds, Stopping threads took: 0.0002145 seconds
 18663.749: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 9663676416 bytes, allocation request: 4415064 bytes, threshold: 9663676380 bytes (45.00 %), source: concurrent humongous allocation]
 18663.751: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: requested by GC cause, GC cause: G1 Humongous Allocation]
 18663.751: [G1Ergonomics (Concurrent Cycles) initiate concurrent cycle, reason: concurrent cycle initiation requested]
2020-07-17T13:16:31.875+0000: 18663.751: [GC pause (G1 Humongous Allocation) (young) (initial-mark)

如果您观察到频繁的Humongous对象分配,和/或与之相关的长时间GC暂停,则下一步是确定谁分配/管理此类对象。例如,可以使用JXRay工具通过堆转储分析来完成此操作。该工具具有“检查”功能,可查找所有大小为1MB或更大的对象。对于每个这样的对象,它显示了返回到某个GC根目录的最短路径(参考链)。具有相同路径的对象被聚集在一起,这使得在管理许多问题对象的代码中轻松识别“热点”。

对于大型对象检测,强烈建议进行完整的堆转储(即,将所有对象(包括垃圾)而不是仅包含活动对象的堆)转储出去。那是因为某些Humongous对象(例如,序列化和发送消息时使用的临时数组)可能是短暂的,即很快变成垃圾,而不会出现在活动堆转储中。但是,垃圾对象的问题是,根据定义,实时数据结构中没有对它们的引用。因此,在分析完整的堆转储时,通常很难确定曾经创建和管理过谁(应用程序代码的哪一部分)。如果不清楚某个“大型对象”是从哪里来的,则您可能需要从同一应用程序中多次进行完整转储,然后分析所有转储或选择最大的转储,更有可能同时容纳垃圾和生活中的巨大物体。这是一份有关Humongous对象的JXRay报告的摘录:

screen-shot-2020-10-10-at-73704-pm.png

减轻大型物体的问题 有几种方法可以解决或至少缓解此问题:调整GC,更改GC类型,解决根本原因或升级到较新的JDK。

在这种情况下,调整GC意味着增加堆或增加区域大小,-XX:G1HeapRegionSize从而使以前的Humongous对象不再是Humongous对象,而是遵循常规分配路径。但是,后者会减少区域数量,这可能会对GC性能产生负面影响。这也意味着将GC选项与当前工作负载结合在一起(这可能会在将来发生变化并打破您当前的假设)。但是,在某些情况下,这是继续进行的唯一方法。

A more fundamental way to address this problem is to switch to the older Concurrent Mark-Sweep (CMS) garbage collector, via the -XX:+UseParNewGC -XX:+UseConcMarkSweepGC flags (unless you use one of the most recent JDK versions in which this collector is deprecated). CMS doesn't divide the heap into numerous small regions and thus doesn't have a problem handling several-MB objects (though every garbage collector may struggle to free up space for bigger objects, that take, say, 10 percent of the heap). In fact, in relatively old Java versions CMS may perform even better overall than G1, at least if most of the objects that the application creates fall into two categories: very short-lived and very long-lived.

解决根本原因意味着以某种方式更改代码以停止生成Humongous对象。例如,有时您可能会发现Humongous对象是太大而未充分利用的缓冲区,因此可以安全地减小它们的大小。在其他情况下,您可能需要对代码进行更根本的更改,或者考虑用另一个数据库替换有问题的第三方库。

最后,从版本11开始,在最新的JDK中,Humongous对象的问题可能不太严重。在这些JDK中,此类对象是在Young Gen中分配的,因此寿命较短的Humongous对象应更快地被收集。在其他方面也可能会更好地处理它们,从而导致较短的与humunous相关的GC暂停,并且可能在Humunous地区也能够容纳正常对象。


原文链接:http://codingdict.com