1.zgc从jdk11版本开始引入,jdk13开始发布正式版,后续每个jdk版本都对它有一定的优化,到jdk17版本基本稳定没有其他严重问题需要修复。
2.zgc第一制作目标就是降低gc运行期间产生的Stop-The-World(后面简称为 stw)时间,同时它也做到了停顿时间在10ms内。jdk16以后一般可保证在1ms内。
3.zgc最大支持堆内存为32TB 最小支持8MB小堆
4.跟G1相比,对应用程序的吞吐量影响小于15%
5.zgc堆内存分配方式分为 小页面(Small ZPage 简称sp)/中页面(Middle ZPage mp)/大页面(Big ZPage bp) 页面总数量动态伸缩不固定(统称为ZPage)
当前版本ZPage 大小划分为 2M/4M~32M(最大为堆内存3%,如果32M大于3%则缩小,最小为4M)/2^n M
在 JDK 14 之前,ZGC 仅使用单个线程进行堆预接触。这意味着如果堆很大,预接触可能需要很长时间。现在,ZGC使用多个线程来完成这项工作,这大大缩短了启动/预触摸时间。在具有 TB 内存的大型计算机上,这种减少可以转化为启动时间为秒而不是分钟。
预接触:通过参数-XX:+AlwaysPreTouch
告诉 GC 在启动时触摸堆最多-Xms
或-XX:InitialHeapSize
)。这将确保支持堆的内存页 1) 实际分配并且 2) 出现故障。通过在启动时执行此操作,可以避免在应用程序运行并开始接触内存时承担此成本。对于某些应用程序来说,预接触堆可能是一个明智的选择,但与往常一样,这是一个权衡,因为启动时间会延长。
在 JDK 14 之前,ZGC 仅使用单个线程进行堆预接触。这意味着如果堆很大,预接触可能需要很长时间。现在,ZGC使用多个线程来完成这项工作,这大大缩短了启动/预触摸时间。在具有 TB 内存的大型计算机上,这种减少可以转化为启动时间为秒级。
当前及过去版本使用堆预留方式进行保证,zgc运行时一定会有一个空的堆可以作为zgc专用区域使用,从而保证zgc必定可以执行成功而无需重定位。预留内存大小为:heap_reserve = (number_of_gc_worker_threads * 2M) + 32M
小堆:使用小于 128M 的堆并不总是一种很好的体验。主要原因是 ZGC 的堆储备与可用堆的比例变得太大,有时会导致早期OutOfMemoryError
.
由于上述预留内存公式可知:当堆内存越小,则预留空间占比(预留堆内存/总内存 * 100%)越大,所以在本版本中对上述公式进行了一部分优化。
(1)使mp的大小变得动态,在当前及以前版本中根据运行时进行确定,并根据堆大小进行缩放,使单个mp占用内存大小不会超过堆堆3%,保证当堆总量过小时,mp不会启用,如果对象超过sp大小则会分配一个bp进行存储。
(2)调整ZGC工作线程数量,保证堆保留中所需的sp总数不会占用超过堆的2%,最少保留一个sp的预留空间供GC运行使用。堆预留大小现在最多为堆的 5%
(1)泄漏分析器不符合访问堆上对象指针规则,使用ZGC时,从堆加载的对象指针必须通过加载屏障才能取消引用。
(2)泄漏分析器分配了一个本季数据结构 标记位图,该结构与为堆保留的地址空间大小成正比,由于ZGC相对需要地址空间,所以它的扩展性不好。
(1)C2 Just-In-Time 编译器生成的ZGC负载屏障通常时ZGC中报错的根本原因。ZGC的实现方式有时候会导致与C2的某些优化过程的不良交互导致代码运行不理想甚至损坏。
(2)在当前版本中修改了C2生成ZGC负载屏障的方式,基本避免了与优化过程的所有交互,并且获得了堆代码生成的更多控制。如:保证stw轮询指令不能安排在加载指令及关联的加载屏障之间。由此大大提高了ZGC的稳定性
(1)当JVM执行stw操作时,它首先以受控方式停止所有java线程。当所有线程停止,它会继续执行实际的safepoint操作(gc或其他操作),由于java线程在stw操作完成之前都保持停止,所以保持该操作较短时间可以提高应用的响应时间。
(2)time-to-safepoint时间(ttsp)是从JVM命令所有java线程停止指令到它们全部停止的时间。所有线程通常不会立即或同时停止。线程可能需要在执行完不可中断操作后才会停止。ttsp与safepoint操作一样都会导致程序响应时间的延长。
(3)当jvm停止指令发出后,如果有线程在初始化对象或大数组,可能会导致两种情况,延长ttsp时间或者立即触发gc导致jvm崩溃(由于对象内属性还没有完全初始化为0或其他默认值),此优化为在zgc中添加一个隐形根,来对这部分新产生的对象进行收集,防止对象在未初始化完成或者刚初始化完成就被回收的情况产生,从而避免影响stw整体运行时长。
1.zgc从jdk11版本开始引入,jdk13开始发布正式版,后续每个jdk版本都对它有一定的优化,到jdk17版本基本稳定没有其他严重问题需要修复。
2.zgc第一制作目标就是降低gc运行期间产生的Stop-The-World(后面简称为 stw)时间,同时它也做到了停顿时间在10ms内。jdk16以后一般可保证在1ms内。
3.zgc最大支持堆内存为32TB 最小支持8MB小堆
4.跟G1相比,对应用程序的吞吐量影响小于15%
5.zgc堆内存分配方式分为 小页面(Small ZPage 简称sp)/中页面(Middle ZPage mp)/大页面(Big ZPage bp) 页面总数量动态伸缩不固定(统称为ZPage)
当前版本ZPage 大小划分为 2M/4M~32M(最大为堆内存3%,如果32M大于3%则缩小,最小为4M)/2^n M
在 JDK 14 之前,ZGC 仅使用单个线程进行堆预接触。这意味着如果堆很大,预接触可能需要很长时间。现在,ZGC使用多个线程来完成这项工作,这大大缩短了启动/预触摸时间。在具有 TB 内存的大型计算机上,这种减少可以转化为启动时间为秒而不是分钟。
预接触:通过参数-XX:+AlwaysPreTouch
告诉 GC 在启动时触摸堆最多-Xms
或-XX:InitialHeapSize
)。这将确保支持堆的内存页 1) 实际分配并且 2) 出现故障。通过在启动时执行此操作,可以避免在应用程序运行并开始接触内存时承担此成本。对于某些应用程序来说,预接触堆可能是一个明智的选择,但与往常一样,这是一个权衡,因为启动时间会延长。
在 JDK 14 之前,ZGC 仅使用单个线程进行堆预接触。这意味着如果堆很大,预接触可能需要很长时间。现在,ZGC使用多个线程来完成这项工作,这大大缩短了启动/预触摸时间。在具有 TB 内存的大型计算机上,这种减少可以转化为启动时间为秒级。
当前及过去版本使用堆预留方式进行保证,zgc运行时一定会有一个空的堆可以作为zgc专用区域使用,从而保证zgc必定可以执行成功而无需重定位。预留内存大小为:heap_reserve = (number_of_gc_worker_threads * 2M) + 32M
小堆:使用小于 128M 的堆并不总是一种很好的体验。主要原因是 ZGC 的堆储备与可用堆的比例变得太大,有时会导致早期OutOfMemoryError
.
由于上述预留内存公式可知:当堆内存越小,则预留空间占比(预留堆内存/总内存 * 100%)越大,所以在本版本中对上述公式进行了一部分优化。
(1)使mp的大小变得动态,在当前及以前版本中根据运行时进行确定,并根据堆大小进行缩放,使单个mp占用内存大小不会超过堆堆3%,保证当堆总量过小时,mp不会启用,如果对象超过sp大小则会分配一个bp进行存储。
(2)调整ZGC工作线程数量,保证堆保留中所需的sp总数不会占用超过堆的2%,最少保留一个sp的预留空间供GC运行使用。堆预留大小现在最多为堆的 5%
(1)泄漏分析器不符合访问堆上对象指针规则,使用ZGC时,从堆加载的对象指针必须通过加载屏障才能取消引用。
(2)泄漏分析器分配了一个本季数据结构 标记位图,该结构与为堆保留的地址空间大小成正比,由于ZGC相对需要地址空间,所以它的扩展性不好。
(1)C2 Just-In-Time 编译器生成的ZGC负载屏障通常时ZGC中报错的根本原因。ZGC的实现方式有时候会导致与C2的某些优化过程的不良交互导致代码运行不理想甚至损坏。
(2)在当前版本中修改了C2生成ZGC负载屏障的方式,基本避免了与优化过程的所有交互,并且获得了堆代码生成的更多控制。如:保证stw轮询指令不能安排在加载指令及关联的加载屏障之间。由此大大提高了ZGC的稳定性
(1)当JVM执行stw操作时,它首先以受控方式停止所有java线程。当所有线程停止,它会继续执行实际的safepoint操作(gc或其他操作),由于java线程在stw操作完成之前都保持停止,所以保持该操作较短时间可以提高应用的响应时间。
(2)time-to-safepoint时间(ttsp)是从JVM命令所有java线程停止指令到它们全部停止的时间。所有线程通常不会立即或同时停止。线程可能需要在执行完不可中断操作后才会停止。ttsp与safepoint操作一样都会导致程序响应时间的延长。
(3)当jvm停止指令发出后,如果有线程在初始化对象或大数组,可能会导致两种情况,延长ttsp时间或者立即触发gc导致jvm崩溃(由于对象内属性还没有完全初始化为0或其他默认值),此优化为在zgc中添加一个隐形根,来对这部分新产生的对象进行收集,防止对象在未初始化完成或者刚初始化完成就被回收的情况产生,从而避免影响stw整体运行时长。
(1)在jdk14前,hotspot要求将堆放置在连续堆地址空间中,其中一些常见操作,如:确定指针指向堆回变得简单而高校。但是当zgc运行时要将对象放置在指定地址区间内时,此时如果该范围内中已经有部分被占用可能回出现其他问题。在jdk14中针对此部分进行了修改,当出现地址空间中某一个部分被占用,则zgc将拆分堆为两个或多个地址空间来避免此类问题产生。
(1)分配 Java 对象通常非常快。新对象的分配方式取决于您使用的 GC。在 ZGC 中,分配路径经过多个层。绝大多数分配由第一层满足,速度非常快。当所有先前的层都无法满足分配时,分配才会在最后一层结束。ZGC 将要求操作系统提交更多内存来扩展堆。如果这也失败,或者我们已经达到最大堆大小(-Xmx
),那么将抛出一个异常OutOfMemoryError
。
(2)在 JDK 15 之前,ZGC 在提交(和取消提交)内存时持有全局锁。这意味着在任何给定时间只有一个线程可以扩展(或收缩)堆。提交和取消提交内存也是相对昂贵的操作,可能需要一些时间才能完成。在 JDK 15 中,分配路径的这一部分被重新设计,以便在提交和取消提交内存时不再持有该锁。结果,在最后一层进行分配的平均成本降低了,并且该层处理并发分配的能力显着提高。
(1)ZGC 的取消提交功能最初是在 JDK 13 中引入的。该机制允许 ZGC 取消提交未使用的内存以缩小堆,并将未使用的内存返回给操作系统以供其他进程使用。为了使内存有资格取消提交,它必须在一段时间内未使用(默认为 300 秒,由 控制-XX:ZUncommitDelay=
)。如果稍后某个时候需要更多内存(新的ZPage分配),那么 ZGC 将提交新内存以再次增长堆。
(2)取消提交内存是一项相对昂贵的操作,完成此操作所需的时间往往会随着您所操作的内存的大小而变化。在 JDK 15 之前,如果 ZGC 发现 2MB 或 2TB 内存符合取消提交的条件,它仍然只会向操作系统发出单个取消提交操作。事实证明,这可能存在问题,因为取消提交大量内存(例如数百 GB 或 TB)可能需要相当长的时间。在此期间,内存压力可能会发生巨大变化,但 ZGC 无法终止或修改中途未提交的操作。如果内存压力增加,ZGC 首先必须等待任何正在进行的未提交操作完成,然后立即再次提交部分内存。
(3)JDK 15 中重新设计了取消提交机制,以增量方式取消提交内存。ZGC 现在将向操作系统发出许多较小的取消操作,而不是单个取消提交操作。这允许及时检测到内存压力的变化,并在运行中终止或修改未提交的进程
(1)非均匀内存访问(UNMA):算机内存设计,NUMA 尝试通过为每个处理器提供单独的内存来解决此问题,从而避免多个处理器尝试寻址同一内存时对性能造成的影响。其中内存访问时间取决于相对处理器堆内存位置。在NUMA下,处理器可以比非本地内存更快的访问自己的本地内存。NUMA的优势仅限于特定的工作负载,特别是在数据通常与某些任务或用户密切相关的服务中。
(2)ZGC 在 Linux 上始终支持NUMA,从某种意义上说,当 Java 线程分配对象时,该对象最终将位于 Java 线程正在执行的 CPU 的本地内存中。在 NUMA 计算机上,访问 CPU 本地内存会降低 内存延迟,从而提高整体性能。然而,ZGC 的 NUMA 意识只有在使用大页面时才能充分发挥其潜力 ( -XX:+UseLargePages
)。这个问题在 JDK 15 中得到了解决,而且无论是否使用大页面,ZGC 的 NUMA 意识现在总是能充分发挥作用。
-XX:AllocateHeapAt参数配置。
此选项将获取文件系统的路径并使用内存映射来实现在内存设备上分配对象堆的预期结果。现有的与堆相关的标志,如
-Xmx
、-Xms
等,以及与垃圾收集相关的标志将继续像以前一样工作。
6.压缩类指针
(1)在HotSpot中,所有Java对象都有一个由两个字段组成的标头:一个标记字和一个类指针。在64位cpu上,这两个字段通常是64位,其中类指针式指向描述对象类的内存的普通指针。压缩类指针功能,可通过配置 -XX:+UseCompressedClassPointers
完成,通过见效所有对象标头的大小来帮助减少总体堆使用量。它通过将类指针字段压缩为32位来实现这一点。压缩类指针不是普通指针,而是压缩类空间的偏移量,该空间具有已知的基地址。来找回真实的类指针JVM只是将压缩类指针添加到压缩类空间的基地址中。
(2)压缩类指针功能的实现历来与压缩Oops功能相关联,如果不启用压缩Oops则无法启用压缩类指针。由于ZGC目前不支持压缩Oops所以在当前版本中,压缩类指针和压缩Oops的依赖关系被打破,从而可以使用 压缩类指针。
(1)HotSpot中的数据共享(CDS)功能有助于减少JVM多个实例之间的启动时间和内存占用。此功能仅在启用Oops功能时才有效( -XX:+UseCompressedOops
)。在JDK15中,类数据共享得到了增强,在禁用压缩Oops功能时也可以工作。因此,类数据共享可以与ZGC很好的配合使用。
如何使用,有什么作用,是否可以在当前系统中应用
(1)在当前版本中,stw时间 从10ms内被降低为1ms内,一般在50um~500um之间
(2)在jdk16之前,ZGC暂停时间仍然会随着根节点集的大小而变化,在stw阶段扫描线程堆栈,这意味着java应用程序中线程数量越多,暂停时间越长。如果这些线程具有很深的调用堆栈,则暂停时间会更长(root可达性分析过程)。从当前版本开始,线程堆栈扫描通过并发完成,与java应用并行执行。
(3)在线程运行时浏览线程堆栈需要通过堆栈水印屏障完成,这是一种机制,可以防止java线程在不检查这样做是否安全的情况下返回到堆栈帧。这是一种廉价的检查,在方法返回时被彻底额到现有的安全点(ttsp)检查中。它可以强制java线程采取某州形式的操作,使堆栈帧返回前进入安全状态。每个线程都有一个或多个堆栈水印,这些水印告知负载屏障在不进行任何特殊操作的情况下可以安全的在堆栈中走多远。使所有线程堆栈进入安全状态的工作通常由一个或多个gc线程处理,但由于这是并发完成,所以当线程返回到GC尚未达到的帧则需要线程进行修复。(并发线程堆栈处理:https://openjdk.org/jeps/376)
下图为jdk15与jdk16 官方测试数据
(1)在jdk16中,ZGC获得了堆就地重定位的支持,此功能有助于避免OutOfMemoryError产生,通常ZGC会通过将对象从稀疏堆区移动到另一个或多个整理堆区从而释放内存。这种方式有一个明显缺点就是需要一定的当前可用内存用于gc使用才能启动重定位过程,如果堆已满,那么将无法移动对象。
(2)在jdk16前ZGC通过堆预留的方式解决此问题,堆预留时一组被ZGC锁定的堆空间,无法被java线程分配。在重定位对象时,只允许gc本身使用堆预留空间,保证在GC执行时一定有空间可用。
(3)由于进行重定位的 Java 线程无法使用堆保留,因此无法硬保证重定位过程能够完成,因此 GC 无法回收足够的内存,从而产生内存溢出。而就地重定位则取消了这一预留区域,然而,就地压缩堆也存在一些挑战,并且通常会带来开销。例如,现在移动对象的顺序非常重要,否则您可能会面临覆盖尚未移动的对象的风险。这需要 GC 线程之间进行更多协调,不利于并行处理,并且还会影响 Java 线程在代表 GC 重新定位对象时可以做什么和不能做什么。
(4)由于上述两种方法各有优缺点,所以在当前版本中,添加了动态开关来根据当前情况来进行两种方案的互相切换,甚至可在一次gc中切换多次。以此来保证不会在因为堆空间不足导致在gc过程中触发内存溢出情况。
(1)当ZGC重新定位对象时,该对象的心地址会记录在转发飙中,转发表时分配在java堆外部的数据结构。选择作为重定位集(需要压缩及释放内存的堆区域集)一部分堆每个堆区都会获得一个相关的转发表。
(2)在jdk16前,当重定位集非常大时,转发表的分配和初始化可能会占用整个GC周期时间的很大一部分。重定位集的大小与重定位期间需要移动的对象数量有关。
(3)在当前版本中,ZGC将批量分配转发表,现在我们不在进行多次调用来为每个表分配信的内存,而是执行一次调用来一次行分配所有表所需的所有内存。这有助于避免通常的分配开销和潜在的锁征用,并显著减少分配这些表所需的时间。
(4)转发表的初始化也是一个瓶颈,转发表时一个哈希表,因此初始化它意味着设置一个小标头并将转发表条目的数组清零。从当前版本开始ZGC开始使用多线程并行执行初始化过程,大大减少了分配和初始化转发表时长。
(1)在JDK 17 之前,ZGC 忽略-XX:+UseDynamicNumberOfGCThreads
并始终使用固定数量的线程。在 JVM 启动期间,ZGC 使用启发式方法来决定固定数字 ( -XX:ConcGCThreads
) 应该是什么。这个数字一旦设定就不会再改变。从 JDK 17 开始,ZGC 现在尊重 -XX:+UseDynamicNumberOfGCThreads
并尝试使用尽可能少的线程,但有足够的线程来继续以其创建的速度收集垃圾。这有助于避免使用超出所需的 CPU 时间,从而为 Java 线程提供更多的 CPU 时间。
(2)注意,启用此功能后,含义-XX:ConcGCThreads
从“使用这么多线程”更改为“最多使用这么多线程”。但除非您有非常规的工作负载,否则通常不需要修改,-XX:ConcGCThreads
. ZGC 的启发式方法将根据您运行的系统的大小为您选择一个合适的最大线程数
(3)如果不需要此功能或者相禁用动态gc线程数可通过 -XX:-UseDynamicNumberOfGCThreads 禁用。
(1)使用ZGC时,终止Java进程并不总是立即生效的,有时候需要一段时间才能真正终止,由于JVM关闭序列需要与GC协调,以便GC停止执行并进入安全状态。而过去版本中如果GC正在运行则JVM需要等待GC执行完毕,在当前版本中ZGC可立即终止当前正在进行的GC操作以快速达到安全状态。
(1)ZGC 进行条纹标记。这是指堆被划分为条带,并且每个 GC 线程被分配来标记这些条带之一中的对象。这有助于最大限度地减少 GC 线程之间的共享状态,并使标记过程对缓存更加友好,因为两个 GC 线程不会标记堆的同一部分中的对象。这种方法还可以在 GC 线程之间实现自然的工作平衡,因为条带中的工作量往往大致相同
(2)在 JDK 17 之前,ZGC 的标记严格遵循条带化。如果 GC 线程在跟踪对象图时遇到一个对象引用,该对象引用指向不属于其分配的条带的堆的一部分,则该对象引用将被放置在与该另一个关联的线程本地标记堆栈上条纹。一旦堆栈满了(254 个条目),它就会被移交给 GC 线程,该线程被分配来处理该条带的标记。加载对尚未标记的对象的对象引用的 Java 线程会执行相同的操作,只不过它始终将对象引用放在关联的线程本地标记堆栈上,并且本身从不执行任何实际的标记工作。这种方法适用于大多数工作负载,但也存在一个病态问题。如果您有一个具有一个或多个 N:1 关系的对象图,其中 N 是一个非常大的数字,那么您可能会冒着为标记堆栈使用大量内存(如许多 GB)的风险。我们一直都知道这可能是一个问题,您可以编写一个小型综合测试来引发它,但我们从未真正遇到过暴露它的现实世界工作负载。也就是说,直到来自腾讯的 OpenJDK 贡献者报告说他们遇到了这个问题。所以,是时候对此做点什么了。
(3)JDK 17 中的修复涉及通过以下方式放松严格的条带化:
(4)这些调整有助于阻止病态 N:1 情况下标记堆栈内存的过度使用,在这种情况下,GC 线程一遍又一遍地遇到相同的对象引用,将大量重复的对象引用推送到标记堆栈上。重复是没有用的,因为一个对象只需要标记一次。通过在推送之前进行标记,并且仅推送之前未标记的对象,可以停止生成重复项。
(5)我们最初有点不愿意这样做,因为 GC 线程现在正在执行原子比较和交换操作,以标记内存中属于其他 GC 线程被分配处理的条带的对象。这打破了严格的条带化,使其不太适合缓存。Java 线程现在还执行原子加载来查看对象是否被标记,这是它们以前没有做过的。与此同时,GC 线程完成的其他工作(扫描/跟踪对象字段以及跟踪每个堆区域的活动对象/字节数)仍然遵循严格的条带化。最终,基准测试表明我们最初的担忧是没有根据的。GC 标记时间不受影响,并且对 Java 线程的影响也不明显。另一方面,我们现在有一个更强大的标记方案,不容易出现过多的内存使用情况。
GarbageCollectorMXBean
s,以提供有关GC 周期和GC 暂停的信息(1)GarbageCollectorMXBean 提供有关 GC 的信息。通过此 bean,应用程序可以提取摘要信息(到目前为止完成的 GC 次数、执行 GC 所花费的累计时间等)并侦听GarbageCollectionNotificationInfo 通知以获取有关各个 GC 的更细粒度的信息(GC 原因、开始时间、结束时间) , ETC)。
(2)在 JDK 17 之前,ZGC 发布了一个名为ZGC
. 该 bean 提供有关 ZGC循环的信息。一个周期包括从开始到结束的所有 GC 阶段。大多数阶段是并发的,但有些阶段是“停止世界”暂停。虽然有关周期的信息很有用,但您可能还想知道在 GC 上花费了多少时间用于 Stop-The-World 暂停。单个 bean 无法提供此信息ZGC
。为了解决这个问题,ZGC 现在发布了两种 Bean,一种称为 ,ZGC Cycles
另一种称为ZGC Pauses
。顾名思义,每个 bean 提供的信息分别映射到Cycles和Pauses。
字符串重复数据删除是一项 JVM 功能 ( -XX:+UseStringDeduplication
),已经存在很长一段时间了。它通过自动删除支持 String 对象的相同字符数组来帮助减少 Java 堆内存的使用。例如,如果堆上存在两个 String 对象,并且它们都指向包含字符“Java”的后备数组,则其中一个 String 对象将被修改,以便两个 String 对象指向同一个数组,而另一个数组将被修改。变得不可访问并接受垃圾收集。对于某些应用程序来说,这可以帮助减少相当多的堆内存使用量,因为 String 对象可以占据 Java 堆的重要部分,并且在这些对象中查找重复项是相当常见的。
当GC卸载未使用的类和已编译的方法时,需要清理一些内联缓存,以便它们不再引用任何卸载的实体。事实证明,这个补丁包含一个小但重要的编辑错误,其中缩进和范围混淆了。仅通过查看补丁很难发现该错误,因为有问题的代码也被移动了。这个错误导致一些内联缓存被错误地清理。然而,不正确的清理并没有导致任何明显的问题,比如 JVM 崩溃。相反,它引发了一个恶性循环,GC 和 Java 线程在如何清理这些缓存方面存在分歧并发生争执。最终结果是,在某些情况下,类卸载可能需要很长时间才能完成。由于该问题的根本原因是 GC 与并发运行的 Java 线程之间的不良交互,因此它只影响进行并发类卸载的 GC(例如 ZGC)。进行 Stop-the-World 类卸载的 GC(例如 SerialGC)目前此功能也已经迁移到jdk17中。
(1)g1分为2048个region 每个region为 2M/4M/8M
(2)zgc分为小页面/中页面/大页面没有固定数量 页面大小为 2M(存放小于256kb对象)/4M(存放大雨等于256kb小于4mb对象)/2^n M(大于等于4mb对象),一个大页面只会分配一个大对象
(3)g1执行过程为
初始标记
并发标记
最终标记(会stw)
筛选回收
(4)zgc执行过程为
初始标记:记录下gc roots直接引用的对 会stw
并发标记
最终标记:修复一些在并发标记过程中垃圾状态出现变化的对象 会stw
并发预分配
并发初始重分配
并发重分配 会stw
并发重映射
(5)zgc在并发重分配的时候,每进行一个对象的复制移动会对其颜色指针的Remapped标识赋值,标识这个指针被gc过,并且还会为其加一个读屏障,使得用户线程访问这个对象时可以知道这个对象的地址被改变了,程序就应该暂停一下,先更新一下地址,再进行访问值的操作,正是因为Load Barriers的存在,所以会导致配置ZGC的应用的吞吐量会变低。
(6)对象头的 Mark Word
(1)在jdk14前,hotspot要求将堆放置在连续堆地址空间中,其中一些常见操作,如:确定指针指向堆回变得简单而高校。但是当zgc运行时要将对象放置在指定地址区间内时,此时如果该范围内中已经有部分被占用可能回出现其他问题。在jdk14中针对此部分进行了修改,当出现地址空间中某一个部分被占用,则zgc将拆分堆为两个或多个地址空间来避免此类问题产生。
(1)分配 Java 对象通常非常快。新对象的分配方式取决于您使用的 GC。在 ZGC 中,分配路径经过多个层。绝大多数分配由第一层满足,速度非常快。当所有先前的层都无法满足分配时,分配才会在最后一层结束。ZGC 将要求操作系统提交更多内存来扩展堆。如果这也失败,或者我们已经达到最大堆大小(-Xmx
),那么将抛出一个异常OutOfMemoryError
。
(2)在 JDK 15 之前,ZGC 在提交(和取消提交)内存时持有全局锁。这意味着在任何给定时间只有一个线程可以扩展(或收缩)堆。提交和取消提交内存也是相对昂贵的操作,可能需要一些时间才能完成。在 JDK 15 中,分配路径的这一部分被重新设计,以便在提交和取消提交内存时不再持有该锁。结果,在最后一层进行分配的平均成本降低了,并且该层处理并发分配的能力显着提高。
(1)ZGC 的取消提交功能最初是在 JDK 13 中引入的。该机制允许 ZGC 取消提交未使用的内存以缩小堆,并将未使用的内存返回给操作系统以供其他进程使用。为了使内存有资格取消提交,它必须在一段时间内未使用(默认为 300 秒,由 控制-XX:ZUncommitDelay=
)。如果稍后某个时候需要更多内存(新的ZPage分配),那么 ZGC 将提交新内存以再次增长堆。
(2)取消提交内存是一项相对昂贵的操作,完成此操作所需的时间往往会随着您所操作的内存的大小而变化。在 JDK 15 之前,如果 ZGC 发现 2MB 或 2TB 内存符合取消提交的条件,它仍然只会向操作系统发出单个取消提交操作。事实证明,这可能存在问题,因为取消提交大量内存(例如数百 GB 或 TB)可能需要相当长的时间。在此期间,内存压力可能会发生巨大变化,但 ZGC 无法终止或修改中途未提交的操作。如果内存压力增加,ZGC 首先必须等待任何正在进行的未提交操作完成,然后立即再次提交部分内存。
(3)JDK 15 中重新设计了取消提交机制,以增量方式取消提交内存。ZGC 现在将向操作系统发出许多较小的取消操作,而不是单个取消提交操作。这允许及时检测到内存压力的变化,并在运行中终止或修改未提交的进程
(1)非均匀内存访问(UNMA):算机内存设计,NUMA 尝试通过为每个处理器提供单独的内存来解决此问题,从而避免多个处理器尝试寻址同一内存时对性能造成的影响。其中内存访问时间取决于相对处理器堆内存位置。在NUMA下,处理器可以比非本地内存更快的访问自己的本地内存。NUMA的优势仅限于特定的工作负载,特别是在数据通常与某些任务或用户密切相关的服务中。
(2)ZGC 在 Linux 上始终支持NUMA,从某种意义上说,当 Java 线程分配对象时,该对象最终将位于 Java 线程正在执行的 CPU 的本地内存中。在 NUMA 计算机上,访问 CPU 本地内存会降低 内存延迟,从而提高整体性能。然而,ZGC 的 NUMA 意识只有在使用大页面时才能充分发挥其潜力 ( -XX:+UseLargePages
)。这个问题在 JDK 15 中得到了解决,而且无论是否使用大页面,ZGC 的 NUMA 意识现在总是能充分发挥作用。
-XX:AllocateHeapAt参数配置。
此选项将获取文件系统的路径并使用内存映射来实现在内存设备上分配对象堆的预期结果。现有的与堆相关的标志,如
-Xmx
、-Xms
等,以及与垃圾收集相关的标志将继续像以前一样工作。
6.压缩类指针
(1)在HotSpot中,所有Java对象都有一个由两个字段组成的标头:一个标记字和一个类指针。在64位cpu上,这两个字段通常是64位,其中类指针式指向描述对象类的内存的普通指针。压缩类指针功能,可通过配置 -XX:+UseCompressedClassPointers
完成,通过见效所有对象标头的大小来帮助减少总体堆使用量。它通过将类指针字段压缩为32位来实现这一点。压缩类指针不是普通指针,而是压缩类空间的偏移量,该空间具有已知的基地址。来找回真实的类指针JVM只是将压缩类指针添加到压缩类空间的基地址中。
(2)压缩类指针功能的实现历来与压缩Oops功能相关联,如果不启用压缩Oops则无法启用压缩类指针。由于ZGC目前不支持压缩Oops所以在当前版本中,压缩类指针和压缩Oops的依赖关系被打破,从而可以使用 压缩类指针。
(1)HotSpot中的数据共享(CDS)功能有助于减少JVM多个实例之间的启动时间和内存占用。此功能仅在启用Oops功能时才有效( -XX:+UseCompressedOops
)。在JDK15中,类数据共享得到了增强,在禁用压缩Oops功能时也可以工作。因此,类数据共享可以与ZGC很好的配合使用。
如何使用,有什么作用,是否可以在当前系统中应用
(1)在当前版本中,stw时间 从10ms内被降低为1ms内,一般在50um~500um之间
(2)在jdk16之前,ZGC暂停时间仍然会随着根节点集的大小而变化,在stw阶段扫描线程堆栈,这意味着java应用程序中线程数量越多,暂停时间越长。如果这些线程具有很深的调用堆栈,则暂停时间会更长(root可达性分析过程)。从当前版本开始,线程堆栈扫描通过并发完成,与java应用并行执行。
(3)在线程运行时浏览线程堆栈需要通过堆栈水印屏障完成,这是一种机制,可以防止java线程在不检查这样做是否安全的情况下返回到堆栈帧。这是一种廉价的检查,在方法返回时被彻底额到现有的安全点(ttsp)检查中。它可以强制java线程采取某州形式的操作,使堆栈帧返回前进入安全状态。每个线程都有一个或多个堆栈水印,这些水印告知负载屏障在不进行任何特殊操作的情况下可以安全的在堆栈中走多远。使所有线程堆栈进入安全状态的工作通常由一个或多个gc线程处理,但由于这是并发完成,所以当线程返回到GC尚未达到的帧则需要线程进行修复。(并发线程堆栈处理:JEP 376: ZGC: Concurrent Thread-Stack Processing)
下图为jdk15与jdk16 官方测试数据
(1)在jdk16中,ZGC获得了堆就地重定位的支持,此功能有助于避免OutOfMemoryError产生,通常ZGC会通过将对象从稀疏堆区移动到另一个或多个整理堆区从而释放内存。这种方式有一个明显缺点就是需要一定的当前可用内存用于gc使用才能启动重定位过程,如果堆已满,那么将无法移动对象。
(2)在jdk16前ZGC通过堆预留的方式解决此问题,堆预留时一组被ZGC锁定的堆空间,无法被java线程分配。在重定位对象时,只允许gc本身使用堆预留空间,保证在GC执行时一定有空间可用。
(3)由于进行重定位的 Java 线程无法使用堆保留,因此无法硬保证重定位过程能够完成,因此 GC 无法回收足够的内存,从而产生内存溢出。而就地重定位则取消了这一预留区域,然而,就地压缩堆也存在一些挑战,并且通常会带来开销。例如,现在移动对象的顺序非常重要,否则您可能会面临覆盖尚未移动的对象的风险。这需要 GC 线程之间进行更多协调,不利于并行处理,并且还会影响 Java 线程在代表 GC 重新定位对象时可以做什么和不能做什么。
(4)由于上述两种方法各有优缺点,所以在当前版本中,添加了动态开关来根据当前情况来进行两种方案的互相切换,甚至可在一次gc中切换多次。以此来保证不会在因为堆空间不足导致在gc过程中触发内存溢出情况。
(1)当ZGC重新定位对象时,该对象的心地址会记录在转发飙中,转发表时分配在java堆外部的数据结构。选择作为重定位集(需要压缩及释放内存的堆区域集)一部分堆每个堆区都会获得一个相关的转发表。
(2)在jdk16前,当重定位集非常大时,转发表的分配和初始化可能会占用整个GC周期时间的很大一部分。重定位集的大小与重定位期间需要移动的对象数量有关。
(3)在当前版本中,ZGC将批量分配转发表,现在我们不在进行多次调用来为每个表分配信的内存,而是执行一次调用来一次行分配所有表所需的所有内存。这有助于避免通常的分配开销和潜在的锁征用,并显著减少分配这些表所需的时间。
(4)转发表的初始化也是一个瓶颈,转发表时一个哈希表,因此初始化它意味着设置一个小标头并将转发表条目的数组清零。从当前版本开始ZGC开始使用多线程并行执行初始化过程,大大减少了分配和初始化转发表时长。
(1)在JDK 17 之前,ZGC 忽略-XX:+UseDynamicNumberOfGCThreads
并始终使用固定数量的线程。在 JVM 启动期间,ZGC 使用启发式方法来决定固定数字 ( -XX:ConcGCThreads
) 应该是什么。这个数字一旦设定就不会再改变。从 JDK 17 开始,ZGC 现在尊重 -XX:+UseDynamicNumberOfGCThreads
并尝试使用尽可能少的线程,但有足够的线程来继续以其创建的速度收集垃圾。这有助于避免使用超出所需的 CPU 时间,从而为 Java 线程提供更多的 CPU 时间。
(2)注意,启用此功能后,含义-XX:ConcGCThreads
从“使用这么多线程”更改为“最多使用这么多线程”。但除非您有非常规的工作负载,否则通常不需要修改,-XX:ConcGCThreads
. ZGC 的启发式方法将根据您运行的系统的大小为您选择一个合适的最大线程数
(3)如果不需要此功能或者相禁用动态gc线程数可通过 -XX:-UseDynamicNumberOfGCThreads 禁用。
(1)使用ZGC时,终止Java进程并不总是立即生效的,有时候需要一段时间才能真正终止,由于JVM关闭序列需要与GC协调,以便GC停止执行并进入安全状态。而过去版本中如果GC正在运行则JVM需要等待GC执行完毕,在当前版本中ZGC可立即终止当前正在进行的GC操作以快速达到安全状态。
(1)ZGC 进行条纹标记。这是指堆被划分为条带,并且每个 GC 线程被分配来标记这些条带之一中的对象。这有助于最大限度地减少 GC 线程之间的共享状态,并使标记过程对缓存更加友好,因为两个 GC 线程不会标记堆的同一部分中的对象。这种方法还可以在 GC 线程之间实现自然的工作平衡,因为条带中的工作量往往大致相同
(2)在 JDK 17 之前,ZGC 的标记严格遵循条带化。如果 GC 线程在跟踪对象图时遇到一个对象引用,该对象引用指向不属于其分配的条带的堆的一部分,则该对象引用将被放置在与该另一个关联的线程本地标记堆栈上条纹。一旦堆栈满了(254 个条目),它就会被移交给 GC 线程,该线程被分配来处理该条带的标记。加载对尚未标记的对象的对象引用的 Java 线程会执行相同的操作,只不过它始终将对象引用放在关联的线程本地标记堆栈上,并且本身从不执行任何实际的标记工作。这种方法适用于大多数工作负载,但也存在一个病态问题。如果您有一个具有一个或多个 N:1 关系的对象图,其中 N 是一个非常大的数字,那么您可能会冒着为标记堆栈使用大量内存(如许多 GB)的风险。我们一直都知道这可能是一个问题,您可以编写一个小型综合测试来引发它,但我们从未真正遇到过暴露它的现实世界工作负载。也就是说,直到来自腾讯的 OpenJDK 贡献者报告说他们遇到了这个问题。所以,是时候对此做点什么了。
(3)JDK 17 中的修复涉及通过以下方式放松严格的条带化:
(4)这些调整有助于阻止病态 N:1 情况下标记堆栈内存的过度使用,在这种情况下,GC 线程一遍又一遍地遇到相同的对象引用,将大量重复的对象引用推送到标记堆栈上。重复是没有用的,因为一个对象只需要标记一次。通过在推送之前进行标记,并且仅推送之前未标记的对象,可以停止生成重复项。
(5)我们最初有点不愿意这样做,因为 GC 线程现在正在执行原子比较和交换操作,以标记内存中属于其他 GC 线程被分配处理的条带的对象。这打破了严格的条带化,使其不太适合缓存。Java 线程现在还执行原子加载来查看对象是否被标记,这是它们以前没有做过的。与此同时,GC 线程完成的其他工作(扫描/跟踪对象字段以及跟踪每个堆区域的活动对象/字节数)仍然遵循严格的条带化。最终,基准测试表明我们最初的担忧是没有根据的。GC 标记时间不受影响,并且对 Java 线程的影响也不明显。另一方面,我们现在有一个更强大的标记方案,不容易出现过多的内存使用情况。
GarbageCollectorMXBean
s,以提供有关GC 周期和GC 暂停的信息(1)GarbageCollectorMXBean 提供有关 GC 的信息。通过此 bean,应用程序可以提取摘要信息(到目前为止完成的 GC 次数、执行 GC 所花费的累计时间等)并侦听GarbageCollectionNotificationInfo 通知以获取有关各个 GC 的更细粒度的信息(GC 原因、开始时间、结束时间) , ETC)。
(2)在 JDK 17 之前,ZGC 发布了一个名为ZGC
. 该 bean 提供有关 ZGC循环的信息。一个周期包括从开始到结束的所有 GC 阶段。大多数阶段是并发的,但有些阶段是“停止世界”暂停。虽然有关周期的信息很有用,但您可能还想知道在 GC 上花费了多少时间用于 Stop-The-World 暂停。单个 bean 无法提供此信息ZGC
。为了解决这个问题,ZGC 现在发布了两种 Bean,一种称为 ,ZGC Cycles
另一种称为ZGC Pauses
。顾名思义,每个 bean 提供的信息分别映射到Cycles和Pauses。
字符串重复数据删除是一项 JVM 功能 ( -XX:+UseStringDeduplication
),已经存在很长一段时间了。它通过自动删除支持 String 对象的相同字符数组来帮助减少 Java 堆内存的使用。例如,如果堆上存在两个 String 对象,并且它们都指向包含字符“Java”的后备数组,则其中一个 String 对象将被修改,以便两个 String 对象指向同一个数组,而另一个数组将被修改。变得不可访问并接受垃圾收集。对于某些应用程序来说,这可以帮助减少相当多的堆内存使用量,因为 String 对象可以占据 Java 堆的重要部分,并且在这些对象中查找重复项是相当常见的。
当GC卸载未使用的类和已编译的方法时,需要清理一些内联缓存,以便它们不再引用任何卸载的实体。事实证明,这个补丁包含一个小但重要的编辑错误,其中缩进和范围混淆了。仅通过查看补丁很难发现该错误,因为有问题的代码也被移动了。这个错误导致一些内联缓存被错误地清理。然而,不正确的清理并没有导致任何明显的问题,比如 JVM 崩溃。相反,它引发了一个恶性循环,GC 和 Java 线程在如何清理这些缓存方面存在分歧并发生争执。最终结果是,在某些情况下,类卸载可能需要很长时间才能完成。由于该问题的根本原因是 GC 与并发运行的 Java 线程之间的不良交互,因此它只影响进行并发类卸载的 GC(例如 ZGC)。进行 Stop-the-World 类卸载的 GC(例如 SerialGC)目前此功能也已经迁移到jdk17中。
(1)g1分为2048个region 每个region为 2M/4M/8M
(2)zgc分为小页面/中页面/大页面没有固定数量 页面大小为 2M(存放小于256kb对象)/4M(存放大雨等于256kb小于4mb对象)/2^n M(大于等于4mb对象),一个大页面只会分配一个大对象
(3)g1执行过程为
初始标记
并发标记
最终标记(会stw)
筛选回收
(4)zgc执行过程为
初始标记:记录下gc roots直接引用的对 会stw
并发标记
最终标记:修复一些在并发标记过程中垃圾状态出现变化的对象 会stw
并发预分配
并发初始重分配
并发重分配 会stw
并发重映射
(5)zgc在并发重分配的时候,每进行一个对象的复制移动会对其颜色指针的Remapped标识赋值,标识这个指针被gc过,并且还会为其加一个读屏障,使得用户线程访问这个对象时可以知道这个对象的地址被改变了,程序就应该暂停一下,先更新一下地址,再进行访问值的操作,正是因为Load Barriers的存在,所以会导致配置ZGC的应用的吞吐量会变低。
(6)对象头的 Mark Word