前言
前段时间我们学习jvm的基础结构和gc相关的基础知识,今天我们详细讲讲几大gc。
串行gc
串行 GC 对年轻代使用 mark-copy (标记-复制) 算法,对老年代使用 mark-sweep-compact (标记-清除-整理) 算法。
两者都是单线程的垃圾收集器,不能进行并行处理,所以都会触发全线暂停(STW),停止所有的应用线程因此这种 GC 算法不能充分利用多核 CPU。不管有多少 CPU 内核,JVM 在垃圾收集时都只能使用单个核心。CPU 利用率高,暂停时间长。简单粗暴,就像老式的电脑,动不动就卡死。该选项只适合几百 MB 堆内存的 JVM,而且是单核 CPU 时比较有用。想想 why?
-XX:+USeParNewGC 改进版本的 Serial GC,可以配合 CMS 使用
串行垃圾收集器(Serial Collector)是最基本、历史最悠久的垃圾收集器,它在垃圾收集的过程中会暂停其他所有的工作线程,也就是人们常说的 "Stop-The-World"。串行收集器在进行垃圾收集时只使用一个CPU或一个收集线程完成,它不仅会导致用户当前正在运行的线程停止,还不会效率地利用多核处理器的优势。
在JVM启动时,通过以下参数可以开启串行垃圾收集器:
- Yyoung代:-XX:+UseSerialGC
- Old代:-XX:+UseSerialOldGC
串行垃圾收集器是一款STW(Stop The World)的收集器,当它在运行时,用户线程会被全部挂起,在完成垃圾收集之前不能恢复运行。
串行垃圾收集器的优点是简单高效(与其他收集器的单线程相比),缺点是在进行垃圾收集时需要暂停所有应用线程,因此不适用于对响应时间有较高要求的应用。
适用场景
尽管串行GC停止所有的应用线程,且只有一个线程参与垃圾收集,但在以下几个场景中,串行GC仍然是一个不错的选择:
- 单核处理器环境:对于只有一个或两个处理器的系统,因为并行/并发执行会产生过多的线程切换和同步开销,所以串行垃圾收集器的停顿时间反而更短。
- 对延迟不敏感的后台应用:如果应用能够容忍短暂的停顿时间,比如一些后台的批处理程序,可以选用串行垃圾收集器。
- 内存较小的环境:串行垃圾收集器对内存的管理相对简单高效。如果应用的堆内存不大(约100MB以内),或者对内存利用率要求高的系统中,可能更适合使用串行GC。
- 嵌入式系统:串行垃圾收集器由于它的高效简单性,还被广泛使用在许多嵌入式系统或者Java ME设备中。
要启用串行GC,可以使用JVM参数-XX:+UseSerialGC。
内存分布
假设内存大小为4GB,并以此为基础,以下是一个基于4GB堆内存的串行垃圾收集器(Serial GC)的例子,涉及年轻代和老年代的内存分布:
年轻代内存分布:
- Eden空间:初始占用3GB,用于对象的初始分配。
- Survivor空间:由两个区域组成,每个区域占用512MB(根据具体配置可能会有所差异),例如一个名为From区和另一个名为To区。
老年代内存分布:
- 占用512MB,用于存放经过多次垃圾回收仍然存活的对象。
当Eden空间和To区的内存占用达到一定阈值时,会触发Minor GC,清理Eden空间和From区的无用对象,并将存活的对象复制到To区。当老年代的内存使用率达到一定阈值或需要进行Full GC时,会触发Major GC,清理整个堆空间的垃圾。
并行gc
年轻代和老年代的垃圾回收都会触发 STW 事件在年轻代使用 标记-复制 (mark-copy) 算法,在老年代使用 标记-清除-整理mark-sweepcompact) 算法。
XX:ParallelGCThreads=N 来指定 GC 线程数,其默认值为 CPU 核心数。
并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效使用,能达到更高的吞吐量:
- 在 GC 期间,所有 CPU 内核都在并行清理垃圾,所以总暂停时间更短;
- 在两次 GC 周期的间隔期,没有 GC 线程在运行,不会消耗任何系统资源;
- 一般来说它的吞吐量是最优的,但是吞吐量最优不一定是gc暂停时间最短;
并行垃圾收集器的几个重要JVM参数:
- -XX:+UseParallelGC:启用并行垃圾收集器,这将改变Java虚拟机中默认的垃圾收集器。
- -XX:ParallelGCThreads=n:设置并行垃圾收集器的线程数。通常,我们可以设置这个参数和逻辑处理器数一样。
- -XX:+UseParallelOldGC:指定老年代使用并行回收收集器,并行收集多个线程一起回收老年代空间。
- -XX:MaxGCPauseMillis=n : 这个参数核心目标是控制最大的垃圾收集的暂停时间。它的默认值是一种比较模糊的目标,JVM将会尽可能地(但是不能保证)达到这个暂停时间。
使用并行GC在多处理器(CPU核心数多)并且有大量内存的环境中,可以充分利用硬件资源以获取尽可能高的吞吐量,所以这种GC方式非常适合在后台计算类的应用,没有交互的场景。
适用场景
- 吞吐量优先的应用:并行垃圾收集器在追求系统的吞吐量方面表现出色。对于一些需要最大化处理能力的应用,如数据处理、图像处理、视频流处理等,可以选择并行垃圾收集器来保持高吞吐量。
- 批处理任务:并行垃圾收集器非常适合处理大量数据的批处理任务。这种类型的任务通常对处理速度和系统的资源利用率有较高的要求,而并行垃圾收集器可以利用多核处理器的能力并行处理垃圾回收,减少系统停顿的时间。
- 后台服务应用:对于后台服务应用,如服务器、网络服务、数据库等,这些应用通常需要处理大量的并发请求。并行垃圾收集器可以快速回收垃圾,减少应用程序的停顿时间,提高整体的系统响应能力。
- 大内存堆应用:由于并行垃圾收集器能够有效利用多个线程进行垃圾回收,因此它适用于具有大内存堆的应用场景。对于需要使用大内存的应用,如内存数据库、物理模拟、科学计算等,使用并行垃圾收集器可以更快地完成垃圾回收,减少应用停顿的时间。
非适用场景
- 实时系统:对于需要实时响应和低延迟的系统,如交互式GUI应用、高频交易系统等,由于并行垃圾收集器会在GC期间暂停应用线程,这可能导致系统的响应时间不稳定或产生较大的延迟。
- 对象创建和销毁频繁的业务功能:如果业务逻辑中有大量创建和销毁对象的操作,由于并行垃圾收集器会产生较长的停顿时间,导致频繁的GC可能会严重影响业务的性能。
- 低内存环境:如果应用程序的可用内存非常有限,使用并行垃圾收集器可能会导致过多的系统资源被垃圾回收过程占用,导致系统出现更长的停顿时间,以及内存资源的浪费。
- 大对象(Large Object):并行垃圾收集器在处理大对象时可能会造成较大的停顿,因为大对象需要更多的时间来复制或清除,从而影响了整个垃圾回收过程的效率。在有大量大对象的场景下,可以考虑其他垃圾收集器策略。
并行GC中不同回收线程之间是如何协作对堆内存进行回收?
在并行GC中,不同的回收线程会同时工作来对堆内存进行垃圾回收。它们会协同工作以实现高效的垃圾收集。
在年轻代的垃圾回收过程中,会将堆内存分为多个区域(一般是2~16个),每个区域由一个线程负责进行垃圾回收。这些线程会同时进行垃圾标记和复制操作,以提高垃圾回收的效率。垃圾标记阶段会遍历对象图,标记出存活的对象,将存活的对象复制到另一个区域中。最后,回收线程会对其负责区域中的垃圾进行回收。
在老年代的垃圾回收过程中,各个回收线程会使用并行方式进行标记和清除操作,并行进行垃圾回收。垃圾标记阶段会遍历对象图进行标记,标记出存活的对象。然后,在清除阶段,垃圾回收线程会并行清理不再存活的对象,并整理堆空间来解决内存碎片问题。
在并行GC过程中,因为涉及到并行协作,所以需要一定的线程同步和协调机制来确保并发操作的正确性。同时,在进行并行垃圾回收时,也需要考虑到线程之间的负载平衡以及避免线程竞争和死锁等问题。
通过并行化的垃圾回收方式,可以充分利用多个处理器核心,提高垃圾回收的效率和整体吞吐量。然而,需要注意的是,并行化的垃圾回收也会引入一些额外的开销,如线程切换和同步开销。因此,在特定的应用场景中,需要根据实际情况进行性能测试和调优,以确定合适的垃圾收集策略。
内存分布
对于一个4GB的内存堆,下面是一个基于并行GC(Parallel GC)的例子,涉及年轻代和老年代的内存分布:
年轻代内存分布:
- Eden空间:初始占用2GB,用于对象的初始分配。
- Survivor空间:由两个区域组成,每个区域占用256MB(根据具体配置可能会有所差异),例如一个名为From区和另一个名为To区。
老年代内存分布:
- 占用1.5GB,用于存放经过多次垃圾回收仍然存活的对象。
与串行GC相比,并行GC会使用多个线程并行进行垃圾回收,以提高内存回收的效率和吞吐量。
在年轻代的垃圾回收过程中,同时对Eden空间和Survivor区进行回收。当Eden空间和To区的内存占用达到一定阈值时,触发Minor GC,清理Eden空间和From区的无用对象,并将存活的对象复制到To区。
在老年代的垃圾回收过程中,多线程并行进行标记和清除操作。标记阶段会遍历老年代对象图,标记存活的对象。然后,在清除阶段,会并行清理不再存活的对象,并整理堆空间来解决内存碎片问题。
需要注意的是,并行GC的具体内存分布会受到具体的JVM参数配置和垃圾收集器策略的影响。使用相关监控工具和命令(如jstat、jmap等),可以获取更准确的内存使用情况和垃圾回收相关的数据。
视频
链接:https://www.aliyundrive.com/s/CicYB9XtnEK
今天就到这里吧,感觉有用的小伙伴可以点个赞,你的支持就是我更新的最大动力!