知识蒸馏 循环蒸馏_Java垃圾收集蒸馏

知识蒸馏 循环蒸馏

串行,并行,并发,CMS,G1,Young Gen,New Gen,Old Gen,Perm Gen,Eden,Tenured,Survivor Spaces,Safepoints和数百个JVM启动标志。 在尝试从Java应用程序获取所需的吞吐量和延迟的同时调整垃圾收集器时,这是否使您感到困惑? 如果确实如此,那就不用担心,您并不孤单。 描述垃圾收集的文档就像飞机的手册页。 每个旋钮和转盘都有详细的说明,但找不到任何有关如何飞行的指南。 本文将尝试解释为特定工作负载选择和调整垃圾收集算法时的权衡取舍。

重点将放在Oracle Hotspot JVM和OpenJDK收集器上,因为它们是最常用的收集器。 最后,将讨论其他商用JVM以说明替代方案。

权衡

明智的人们不断告诉我们:“您一无所获”。 当我们得到一些东西时,我们通常不得不放弃一些回报。 当涉及到垃圾收集时,我们使用3个主要变量来为收集器设置目标:

  1. 吞吐量:应用程序完成的工作量与在GC中花费的时间之比。 使用‑XX:GCTimeRatio = 99的目标吞吐量 默认值为99,等于1%的GC时间。
  2. 延迟:系统响应事件所花费的时间,该时间受垃圾收集引入的暂停影响。 GC的目标等待时间使用‑XX:MaxGCPauseMillis = <n>暂停。
  3. 内存:我们的系统用于存储状态的内存量,在管理状态时通常会对其进行复制和移动。 应用程序在任何时间点保留的活动对象集称为实时集。 最大堆大小–Xmx <n>是用于设置应用程序可用堆大小的调整参数。

注意:Hotspot通常无法实现这些目标,并且会在没有任何警告的情况下默默地继续运行,因为它已经大大偏离了目标。

延迟是事件之间的分布。 增加平均等待时间以减少最坏情况的等待时间或使其更不频繁,这是可以接受的。 我们不应将“实时”一词解释为意味着最低的延迟。 而是指无论吞吐量如何都具有确定性的延迟。

对于某些应用程序工作负载,吞吐量是最重要的目标。 一个例子是长期运行的批处理作业。 只要可以更快地完成整个作业,那么在进行垃圾收集时是否偶尔将批处理作业暂停几秒钟并不重要。

对于几乎所有其他工作负载,从面向人类的交互式应用程序到金融交易系统,如果系统在几秒钟或几毫秒内无法响应,则可能会带来灾难。 在金融交易中,通常值得牺牲一些吞吐量以换取一致的延迟。 我们可能还会有一些应用程序,这些应用程序受到可用物理内存量的限制,并且必须保持占用空间,在这种情况下,我们必须放弃延迟和吞吐量方面的性能。

权衡通常表现如下:

  • 通过为垃圾回收算法提供更多的内存,可以在很大程度上减少作为摊销成本的垃圾回收成本。
  • 通过包含活动集并保持堆大小较小,可以减少由于垃圾收集而导致的观察到的最坏情况的延迟引发的暂停。
  • 通过管理堆和生成大小以及控制应用程序的对象分配速率,可以减少出现暂停的频率。
  • 通过与应用程序同时运行GC,可以减少较大的暂停频率,有时会牺牲吞吐量。

对象寿命

垃圾收集算法通常经过优化,以期大多数对象的生存期很短,而很少有对象生存期很长。 在大多数应用程序中,生存期很长的对象往往构成随时间分配的对象的很小一部分。 在垃圾收集理论中,这种观察到的行为通常被称为“婴儿死亡率”或“弱代假设”。 例如,循环迭代器通常寿命很短,而静态字符串实际上是永生的。

实验表明,世代垃圾收集器通常可以比非世代收集器支持更大数量级的吞吐量,因此几乎在服务器JVM中广泛使用。 通过分离对象的世代,我们知道新分配对象的区域对于活动对象可能非常稀疏。 因此,收集器在此新区域中清除少量活动对象并将其复制到较旧对象的另一个区域中可以非常有效。 热点垃圾收集器根据生存的GC周期数记录对象的寿命。

注意:如果您的应用程序持续生成许多可以生存很长时间的对象,则可以预期您的应用程序将花费大量时间进行垃圾回收,并希望花费大量时间来调整Hotspot垃圾收集器。 这是由于世代“过滤器”效率降低时发生的GC效率降低,以及导致更频繁地收集更长寿命的世代的成本。 老一辈人稀疏,因此老一辈人收集算法的效率往往要低得多。 世代垃圾收集器通常以两个不同的收集周期运行:次要垃圾收集(收集短期对象)和不那么频繁的主要垃圾收集(收集较旧的区域)。

世界停止事件

在垃圾回收期间,应用程序遭受的暂停是由于所谓的世界停止事件造成的。 为了使垃圾收集器运行,出于实际工程上的原因,有必要定期停止正在运行的应用程序,以便可以管理内存。 根据算法的不同,不同的收集器将在特定的执行点停下世界,并持续不同的时间。 要使应用程序完全停止,必须暂停所有正在运行的线程。 垃圾收集器通过发信号通知线程在到达“安全点”时停止运行来做到这一点,这是程序执行期间所有GC根已知且所有堆对象内容一致的点。 根据线程在做什么,可能需要一些时间才能达到安全点。 安全点检查通常在方法返回和回送边沿上执行,但可以在某些地方进行优化,从而使其在动态上更加罕见。 例如,如果线程正在复制大型数组,克隆大型对象或执行具有有限界限的单调计数循环,则到达安全点可能要花费几毫秒的时间。 安全时间是低延迟应用程序中的重要考虑因素。 通过启用此时间可以浮出水面
‑XX:+ PrintGCApplicationStoppedTime标志以及其他GC标志。

注意对于具有大量正在运行的线程的应用程序,当世界停止事件发生时,随着线程从安全点释放后恢复,系统将承受重大的调度压力。 因此,较少依赖于世界停止事件的算法可能会更有效。

热点堆组织

要了解不同收集器的工作方式,最好探索如何组织Java堆以支持分代收集器。

伊甸园是最初分配大多数对象的区域。 幸存者空间是一个临时存储区,用于存储在伊甸园空间中幸存的对象。 讨论次要收藏时,将描述幸存者空间的使用情况。 总的来说伊登生存空间被称为“ 年轻 ”或“ ”的产生。

寿命足够长的对象最终将提升为使用期限

烫发生成是运行时将其“知道”为有效的对象(例如类和静态字符串)存储的地方。 不幸的是,在许多应用程序中持续使用类加载的常见用法使烫发生成(类是不朽的)背后的动机假设是错误的。 在Java 7中,已将Strings从permgen移至tenured ,而从Java 8中不再存在perm的生成,因此本文将不进行讨论。 大多数其他商业收藏家并不使用单独的烫发空间,而是倾向于将所有长期存在的物品视为永久使用。

注意虚拟空间允许收集器调整区域的大小,以满足吞吐量和延迟目标。 收集器会保留每个收集阶段的统计信息,并相应地调整区域大小,以达到目标。

对象分配

为了避免争用,每个线程都分配有一个线程本地分配缓冲区(TLAB),从该线程中分配对象。 使用TLAB可以避免对象在单个内存资源上的争用,从而使对象分配随线程数扩展。 通过TLAB分配对象是非常便宜的操作; 它只是碰触对象大小的指针,在大多数平台上大约需要10条指令。 Java的堆内存分配比从C运行时使用malloc还要便宜。

注意尽管单个对象分配非常便宜,但必须进行次要收集的速率与对象分配的速率成正比。

当TLAB耗尽时,一个线程只需向Eden空间请求一个新线程。 当伊甸园装满后,便开始小规模收集。

大对象(-XX:PretenureSizeThreshold = n)可能无法容纳在年轻一代中,因此必须在旧一代中进行分配,例如大型数组。 如果将阈值设置为低于TLAB大小,则不会在旧版本中创建适合TLAB的对象。 新的G1收集器以不同的方式处理大型物体,稍后将在其单独的部分中进行讨论。

小型收藏

当伊甸园变满时,将触发次要回收。 这是由新一代的所有活动对象复制到任何一个幸存者空间或终身空间适当地进行。 复制到终身空间被称为晋升或tenuring。 对于足够旧的对象( – XX:MaxTenuringThreshold ),或幸存者空间溢出时,将进行升级

活动对象是应用程序可访问的对象。 任何其他物体都无法到达,因此可以视为死亡 。 在次要集合中,首先通过遵循所谓的GC根目录执行活动对象的复制,然后反复复制可到达生存空间的任何对象。 GC根通常包括来自应用程序和JVM内部静态字段以及线程堆栈框架的引用,所有这些引用均有效指向应用程序的可访问对象图。

在代收集,为新一代的可及对象图中的GC根还包括从一代到新一代的任何引用。 这些引用也必须进行处理,以确保在新一代所有可到达对象生存的小的收集。 通过使用“卡片表”来识别这些跨代参考。 热点卡表是一个字节数组,其中每个字节用于跟踪一代的相应512字节区域中跨代引用的潜在存在。 作为参考被存储到堆,“存储屏障”代码将标志牌,以指示从一代到新一代的电势参考可以在相关联的512字节的堆区存在。 在数据收集过程中,卡表用于扫描这种跨代引用,从而有效地代表了额外的GC根到新的一代。 因此次要集合的显著固定成本成正比一代的大小。

有两个生存空间中的热点的一代,交替在他们的“ 空对空 ”和“ 从空间 ”的角色。 在次要收藏开始时,“ to-space幸存者空间始终是空的,并充当次要收藏的目标副本区域。 先前的次要收藏的目标幸存者空间是“ from-space ”的一部分,“ from-space ”还包括Eden ,在其中可以找到需要复制的活动对象。

GC收集的成本通常是通过复制对象的幸存者终身教授空间的成本控制。 没有生存小的收集对象是有效的自由与处理。 一个小的收集过程中所做的工作是成正比的发现,而不是新一代的大小活动对象的数量。 每次将伊甸园面积扩大一倍时,花在次要收藏上的总时间几乎可以减少一半。 因此可以将内存用于吞吐量。 将Eden大小增加一倍会导致每个收集周期的收集时间增加,但是如果要提升的对象数和一代的大小都恒定,则这相对较小。

注意在热点中, 次要收藏是世界停止的事件。 随着越来越多的活动对象堆越来越大,这正Swift成为一个主要问题。 我们已经开始看到需要同时收集年轻一代以达到暂停时间目标的需求。

主要收藏

主要藏品收集了一代,以便可以从年轻一代中推广物品。 在大多数应用程序中,绝大多数程序状态最终出现在一代。 最大的不同的GC算法存在的一代。 有些会在填满时压缩整个空间,而另一些会与应用程序同时收集以尝试防止填满。

一代的收藏家将尝试预测何时需要收藏,以避免年轻一代的晋升失败。 收集器跟踪一代的填充阈值,并在超过该阈值时开始收集。 如果该阈值不足以满足促销要求,则触发“ FullGC”。 FullGC涉及推广年轻一代的所有活动对象,然后收集和压缩一代。 升级失败是一项非常昂贵的操作,因为必须解开此循环中的状态和升级对象,以便发生FullGC事件。

注意为避免升级失败,您将需要调整旧版本允许容纳升级的填充(‑XX:PromotedPadding = <n>)。

注意当堆需要增长时,会触发FullGC。 通过将–Xms–Xmx设置为相同的值,可以避免这些调整堆大小的FullGC。

除了FullGC, 旧版本的压缩很可能是应用程序将遇到的最大的停顿停顿状态。 该压缩的时间往往与年老空间活动对象的数量呈线性增长。

有时可以通过增加幸存者空间的大小和对象的年龄来降低占位空间的填充率,然后再提升其为占位空间 。 然而,增加生存空间的小集合规模和对象年龄(-XX:MaxTenuringThreshold)推广之前还可以增加在集合的成本和暂停时间由于对未成年人的集合生存空间的增加的拷贝成本。

串行收集器

串行收集器(-XX:+ UseSerialGC)是最简单的收集器,是单处理器系统的不错选择。 它还具有所有收集器中最小的占地面积。 它对次要主要集合都使用一个线程。 使用简单的凹凸指针算法在持久空间中分配对象。 当终身空间已满主要收集被触发。

并联收集器

并行收集器有两种形式。 并行收集器(‑XX:+ UseParallelGC),它使用多个线程来执行Young代的次要收集,并使用单个线程来执行代的主要收集。 Java 7u4以来的默认值是Parallel Old Collector(‑XX:+ UseParallelOldGC),它对次要集合使用多个线程,对主要集合使用多个线程。 使用简单的凹凸指针算法在持久空间中分配对象。 当终身空间已满主要收集被触发。

在多处理器系统上,并行旧收集器将提供所有收集器中最大的吞吐量。 直到发生收集为止,它对正在运行的应用程序没有影响,然后将使用最有效的算法使用多个线程并行收集。 这使得Parallel Old Collector非常适合批处理应用。

收集旧版本的成本受要保留的对象数量的影响要比与堆大小的影响更大。 因此,可以通过提供更多内存并接受较大但较少的收集暂停来提高Parallel Old收集器的效率,以实现更大的吞吐量。

期待与此收集器最快的小集合,因为晋升为终身空间是一个简单的凹凸指针和复制操作。

对于服务器应用程序,Parallel Old收集器应该是第一个调用端口。 但是,如果主要的收集暂停时间超出了您的应用程序所能承受的范围,则您需要考虑使用并发收集器,该并发收集器在应用程序运行时同时收集历时对象。

注意在压缩旧版本的同时,现代硬件上每GB实时数据的暂停时间大约为1-5秒。

并发标记扫描(CMS)收集器

CMS(-XX:+ UseConcMarkSweepGC)收集器在旧版本中运行,以收集在大型收集期间不再可访问的终身对象。 它与应用程序同时运行,目的是在一代中保留足够的可用空间,从而不会发生年轻一代的升级失败。

升级失败将触发FullGC。 CMS遵循多个步骤:

  1. 初始标记<世界各地>:查找GC根目录。
  2. 并发标记:标记GC根目录中的所有可到达对象。
  3. 并发预清理:通过标记检查在并发标记阶段是否已更新对象引用和已提升对象。
  4. 重新标记<stop-the-world>:捕获自Pre-clean阶段以来已更新的对象引用。
  5. 并发扫描:通过回收死对象占用的内存来更新空闲列表。
  6. 并发重置:重置数据结构以供下次运行。

租用对象变得不可访问时,CMS将回收该空间并将其放入空闲列表。 进行促销时,必须在自由列表中搜索要促销的对象的合适大小的Kong。 与Parallel Collector相比,这增加了推广成本,从而增加了Minor收藏的成本。

注意 CMS不是压缩收集器,随着时间的推移,它可能导致旧的碎片化。 对象升级可能会失败,因为大型对象可能不适合旧版本中的可用Kong。 当发生这种情况时,将记录“促销失败”消息,并触发FullGC来压缩实时使用的对象。 对于此类压缩驱动的FullGC,由于CMS使用单个线程进行压缩,因此与使用Parallel Old收集器的主要集合相比,预期的停顿情况会更糟。

CMS通常与应用程序并发,这具有许多含义。 首先,CPU时间由收集器占用,从而减少了可用于应用程序的CPU。 CMS所需的时间与将对象提升到保有空间的数量成线性增长。 其次,对于并发GC周期的某些阶段,必须将所有应用程序线程带入一个安全点,以标记GC根并执行并行重新标记以检查变异。

注意如果应用程序发现使用权对象发生了重大变化,则重新标记阶段可能很重要,在极端情况下,重新标记阶段可能比使用Parallel Old Collector进行完全压缩要花费更长的时间。

CMS使FullGC成为不太频繁的事件,但代价是吞吐量降低,更昂贵的次要收集和更大的占用空间。 与并行收集器相比,吞吐量的降低幅度可能在10%-40%之间,具体取决于提升率。 CMS还需要占用20%的空间,以容纳其他数据结构和“浮动垃圾”,这些并发标记在传递到下一个周期的并发标记期间可能会丢失。

有时可以通过增加年轻一代空间和一代空间的大小来降低高晋升率和由此造成的分裂。

注意:如果 CMS收集速度不足以跟上升级的速度,则CMS可能会遇到“并发模式故障”,这可以在日志中看到。 当收集开始得太晚时可能会导致这种情况,可以通过调整来解决。 但是,当收集率无法跟上某些应用程序的高推广率或高对象突变率时,也会发生这种情况。 如果应用程序的升级率或变异率太高,则您的应用程序可能需要进行一些更改以减轻升级压力。 向这样的系统添加更多的内存有时会使情况变得更糟,因为CMS将需要更多的内存来进行扫描。

垃圾优先(G1)收集器

G1(-XX:+ UseG1GC)是Java 6中引入的新收集器,现已在Java 7中得到正式支持。它是部分并发收集算法,还试图在较小的渐进式停顿中压缩占位空间,以进行尝试并最小化由于碎片而困扰CMS的FullGC事件。 G1是分代收集器,通过将堆划分为可变目的的固定大小区域(而不是出于相同目的的连续区域),从而与其他收集器不同地组织了堆。

G1采用同时标记区域的方法来跟踪区域之间的引用,并将收集集中在具有最大可用空间的区域上。 然后,通过将活动对象疏散到一个空的区域,以停下来的暂停增量收集这些区域,从而在此过程中进行压缩。 大于某个区域50%的对象被分配在多个区域中的大型区域中。 在G1下,大型对象的分配和收集可能会非常昂贵,并且迄今为止几乎没有或没有进行任何优化工作。

任何压缩收集器所面临的挑战不是对象的移动,而是对这些对象的引用的更新。 如果从许多区域引用了一个对象,则更新这些引用所花费的时间可能比移动该对象要长得多。 G1通过“记忆集”跟踪区域中的哪些对象具有其他区域的引用。 如果“记住的集合”变大,则G1会显着降低速度。 当将对象从一个区域撤离到另一个区域时,相关的世界停止事件的时间长度往往与需要扫描并可能需要打补丁的参考区域的数量成正比。

维护“已记住的集合”会增加次要集合的成本,从而导致停顿的时间要长于Parallel Old或CMS的次要集合。

G1是目标驱动程序,其时延为–XX:MaxGCPauseMillis = <n> ,默认值= 200ms。 该目标将尽力而为地影响每个周期的工作量。 在几十毫秒内设置目标通常是徒劳的,而在撰写本文时,针对数十毫秒的目标还不是G1的重点。

对于较大的堆,G1是一个很好的通用收集器,当应用程序可以容忍0.5-1.0秒范围内的增量压缩暂停时,G1往往会变得碎片化。 G1倾向于减少CMS看到的最坏情况的停顿的频率,这是因为碎片化的代价是扩展了次要收集的范围和一代的增量压缩。 大多数暂停最终都局限于区域压缩,而不是全部堆压缩。

像CMS一样,G1也可能无法跟上晋升率,并且会退回到世界末日的FullGC。 就像CMS具有“并发模式故障”一样,G1可能会发生疏散故障,在日志中被视为“ 空间溢出”。 当没有空闲区域可将对象撤离时,就会发生这种情况,这类似于升级失败。 如果发生这种情况,请尝试使用更大的堆和更多的标记线程,但是在某些情况下,可能需要更改应用程序以降低分配率。

对于G1来说,一个具有挑战性的问题是处理受欢迎的物体和区域。 当区域中的活动对象没有从其他区域大量引用时,增量停止世界压缩将非常有效。 如果某个对象或区域很受欢迎,则“记住的集合”将很大,G1将尝试避免收集这些对象。 最终,它别无选择,这会导致堆压缩时非常频繁的中长度暂停。

替代并行收集器

CMS和G1通常被称为并发收集器。 当您查看所执行的全部工作时,很显然, 年轻一代,晋升甚至许多一代工作根本不是同时发生的。 CMS是一代大多并发; G1更像是一个停滞不前的增量收集器。 CMS和G1都有重大且定期发生的世界停止事件,以及最坏的情况,通常使它们不适用于严格的低延迟应用程序,例如金融交易或React性用户界面。

可以使用其他收集器,例如Oracle JRockit Real Time,IBM Websphere Real Time和Azul Zing。 JRockit和Websphere收集器在大多数情况下都比CMS和G1具有延迟优势,但是经常遇到吞吐量限制,并且仍然遭受重大的世界停止事件。 Zing是该作者所知的唯一Java收集器,它可以真正地并发进行收集和压缩,同时保持所有代的高吞吐率。 Zing确实有一些毫秒级的世界停止事件,但这些事件是与收集周期中的相移无关的,这些相移与实时设置的大小无关。

对于在包含的堆大小下的高分配率而言,JRockit RT可以实现数十毫秒的典型暂停时间,但有时还必须恢复到完全压缩暂停。 Websphere RT可以通过受限制的分配速率和活动集大小来实现单位毫秒的暂停时间。 通过在所有阶段(包括次要收集期间)并发执行,Zing可以以高分配率实现亚毫秒级的暂停。 无论堆大小如何,Zing都可以保持这种一致的行为,从而使用户可以根据需要应用大堆大小,以适应应用程序吞吐量或对象模型状态需求,而不必担心增加暂停时间。

对于所有针对延迟的并发收集器,您必须放弃一些吞吐量并增加占用空间。 根据并发收集器的效率,您可能会放弃一点吞吐量,但是始终会增加大量占用空间。 如果是真正的并发,几乎没有停滞事件,则需要更多的CPU内核来启用并发操作并保持吞吐量。

注意:分配足够的空间后,所有并发收集器往往会更有效地发挥作用。 作为经验法则,您应该将堆的预算至少为活动集大小的2到3倍,以实现高效操作。 但是,用于维持并发操作的空间需求随应用程序吞吐量以及相关的分配和提升率而增长。 因此,对于更高吞吐量的应用程序,可以保证更高的堆大小与活动集比率。 鉴于当今系统可用的巨大内存空间,在服务器端很少出现问题。

垃圾收集监控和调整

要了解您的应用程序和垃圾收集器的行为方式,请至少使用以下设置启动JVM:

-verbose:gc
-Xloggc:<filename>
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime

然后将日志加载到Chewiebug之类的工具中进行分析。

要查看GC的动态性质,请启动JVisualVM并安装Visual GC插件。 这将使您能够如下所示查看适用于您的应用程序的GC。

为了了解您的应用程序的GC需求,您需要可以重复执行的代表性负载测试。 当您掌握每个收集器的工作方式时,然后以不同的配置运行负载测试作为实验,直到达到吞吐量和延迟目标。 从最终用户的角度衡量延迟很重要。 这可以通过在直方图中捕获每个测试请求的响应时间来实现,您可以在此处了解更多信息。 如果您的延迟峰值超出可接受范围,请尝试将其与GC日志关联,以确定是否是GC问题。 其他问题可能会导致延迟峰值。 另一个值得考虑的有用工具是jHiccup ,它可用于跟踪JVM中以及整个系统中的暂停。

如果延迟高峰是由于GC引起的,则投资调整CMS或G1以查看您的延迟目标是否可以实现。 有时由于高分配和提升率以及极低的延迟要求,这可能无法实现。 GC调整可以成为一项高技能的练习,通常需要更改应用程序以减少对象分配率或对象寿命。 如果是这种情况,则可能需要在时间和花费在GC调整和应用程序更改上的资源之间进行商业平衡,例如,可能需要购买商业并发压缩JVM中的一种,例如JRockit Real Time或Azul Zing。

翻译自: https://www.infoq.com/articles/Java_Garbage_Collection_Distilled/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

知识蒸馏 循环蒸馏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值