JVM基础 - 垃圾回收 -- 02
一.为什么进行垃圾回收(Garbage)
垃圾(Garbage): 程序运行时,在内存中根据一系列规则创建对象。在某一段时间程序使用对象记录的一些信息,之后程序不再使用这些信息。则该对象就是垃圾。
程序运行时,需要不停的创建大量对象,进行程序运行的逻辑处理。如果对象创建后,不进行管理,则最终会将系统内存填满,出现内存溢出(OutOfMemoryError),导致程序无法运行。垃圾回收,就是根据一定的规则,对不再使用的对象进行回收。C/C++ 程序需要程序编写人员手动控制对象的释放。但这个对程序编写人员要求比较高,容易出现失误。Java ,C#,Python 等系列语言提出,由程序自动回收垃圾,减轻程序编写人员的负担。
二. 垃圾标记阶段
程序运行时,会在内存中有创建很多对象。一段时间后,有些对象程序还在使用的,有些对象程序不再使用了。进行垃圾回收之前,需要区分出哪些是不再使用的可回收对象,哪些是还在使用的对象。这个将对象进行区分的过程,就是垃圾标记阶段。
常见的垃圾标记算法
(一): 引用计算算法(Reference counting): 在对象上专门添加一个其他对象引用该对象实例次数的计数变量。每当其他对象引用该对象时,计数变量就加一。当其他对象取消掉对该对象的引用时,计数变量就减一。垃圾回收时,计数变量为0的,即可以直接回收。
优点:算法实现简单,执行高效。
缺点:增加计数存储字段,加大了存储空间。无法解决循环引用计数问题。
Java 垃圾回收器没有使用该算法。Python 使用该算法,通过程序编写人员手动解除引用或使用弱引用(weakref)解决循环依赖问题。
(二): 可达性分析算法(),或根搜索算法(GC Roots),或跟踪性垃圾收集(Tracing Garbage Collection): 以根对象集合(GC Roots)为起始点,从上到下依次遍历对象所有引用的其他对象。只有直接或间接的被根对象所引用,就是在使用的对象,不能被回收。其他没被引用的对象,则是垃圾,可进行回收。Java,C# 使用此算法。
搜索从根对象集合到子引用对象经过的路径被称为 引用链(Reference Chain)。
Java 中以下几种对象是根对象(GC Roots)
1. JAVA虚拟机栈(JVM Stacks)中引用的对象。即各个线程中,在方法栈帧(Stack Frame)中,局部变量表下的引用对象。方法传入的引用参数和方法中创建的引用类型变量。
2. 本地方法栈(Native Method Stacks)内引用的对象。即JNI 方法引用的对象。
3. 类的静态属性引用的对象。类的静态属性保存在方法区中。
4. 常量中引用的对象。JDK8中常量在方法区。例如保存字符串常量池(String Table )的引用。
5. 所有被同步锁(synchronized)持有的对象。
6. JAVA虚拟(JVM)内部持有的对象引用。基本数据类型对象的Class 对象,常驻的异常对象(NullPointerException, OutOfMemoryError),系统类加载器。
7. 记录Java虚拟机内部情况的JMXBean, JVMTI中注册的回调, 本地代码缓存等。
小技巧:Java 使用栈保存变量和对象引用的指针。如果一个引用指针,保存了堆中对象的地址,而自己又不在堆中。则它就是一个根引用(Root)。
暂停世界(Stop The Wold): 在进行对象的可达性分析时,为保存当前程序运行时的所有对象快照,保证分析结果的准确性,不会出现并发修改对象引用问题,这时会将所有用户线程暂停。
三.垃圾清除阶段 - 基础垃圾算法理论
当成功标记出内存中在使用的对象和不使用的对象时,GC就可以进行垃圾回收,释放掉不使用对象占用的内存,为程序继续运行提供空间。
常用垃圾回收算法的基础
标记-清除算法(Mark - Sweep): 当堆中有效空间(available memory)使用完时,就会停止整个程序(Stop The Wold),然后进行标记-清除处理。标记阶段,垃圾收集器(Collector)从根节点开始遍历,标记所有被引用的对象。即对象Header中记录为可达对象。清理阶段,垃圾收集器(Collector)对堆内存从头到尾进行线性遍历,如果发现对象Header没有被标记为可达,则将其回收。该算法,效率不高,执行时需要暂停程序,用户体验差。回收结束,清理的空间是不连续的,容易产生内存碎片,需要维护空闲列表。
复制算法 (Copying):将内存分成两块,每次只使用其中的一块。在进行垃圾回收时,将正在使用的内存中标记为存活的对象全部复制到另一块未使用的内存上,然后将正在使用的内存上所有对象进行清理,交换角色。不停重复上面的步骤,进行垃圾回收。没有清除过程,实现简单,执行高效。复制对象后,保证空间的连续性,不会出现空间碎片。内存使用率低,需要双倍的空间。
特别适合垃圾对象多,存活对象少的场景。例如:Young区的 Survivor0 和 Survivor0 。
标记-压缩算法(Mark - Compact)或标记-清除-压缩算法(Mark-Sweep-Compact):先从根对象从上到下依次遍历,标记出使用的和未使用的对象。然后将使用的对象,压缩到内存一段,按顺序存放。之后清理未使用对象的空间。该算法,无标记-清除算法产生内存碎片问题,也没有复制算法浪费一半内存问题。但效率来说,比标记-清除算法低。移动对象时,被其他对象引用的地址信息也要修改。需要暂停用户程序。
老年代存活对象非常多,非常适合使用此算法。
指针碰撞(Bump the Pointer): 内存可使用空间的连续的,以使用空间和未使用空间在内存两边,同时有个变量专门记录使用空间和未使用空间的分界点地址。当为新对象分配内存地址时,通过修改指针的偏移量将新对象分配到第一个空闲的位置。
分代收集算法(Generational Collecting):不同对象的生命周期不同,使用不同的收集算法,可以提高回收效率。Java中堆分为新生代和老年代,根据各个年代不同,使用不同的算法,回收效率比较高。
年轻代(Young Gen):分配的内存空间较小,生命周期短,存活率低,回收频繁。 复制算法是最快的。
老年代(Old Gen):分配的内存空间较大,生命周期长,存活率高,回收不能太频繁。标记-清除或标记-整理实现。
增量收集算法(Incremental Collecting):在垃圾回收时,一次进行所有垃圾的回收,会导致用户线程暂停过久,影响用户体验。那通过垃圾收集线程和应用程序线程交替进行,改善停顿时间。每次垃圾 回收时只进行一部分区域的空间,然后再切换到应用程序线程。重复以上步骤,直到完成垃圾回收。该算法,会进行频繁的线程切换,导致垃圾总的回收成本变高,造成系统吞吐量基降低。
增量收集算法改善了垃圾收集时,应用程序暂时的时间。(Stop The World)
分区收集算法():垃圾回收时,堆空间越大,一次GC所使用的时间就越久,由GC导致的停顿时间(Stop The World)也就越久。为了更好的控制停顿时间,将堆空间划分成多个小的区块。每次垃圾回收时,根据配置的停顿时间,选择性的收集几个区块。从而改善每次停顿时间的长度。
如上这些算法是垃圾回收算法的基础理论。Hotspot 虚拟机使用的垃圾回收算法是由基于这些基础概念演变的复合算法。
安全点(Safepoint):程序在进行垃圾回收时,并不是在所有位置都可以暂停的。需要程序执行到指定位置,才可以暂停,而这个特定位置,称为 安全点。通常会根据 是否具有让程序长时间执行的特征 为标准选择安全点。Java 中选择方法调用,循环跳转 和 异常处理跳转 等位置。 主动式中断:程序运行时,设置中断标志,程序每次运行到安全点,都区检查中断标志。如果标志设置为真,则挂起程序。 抢断时中断: 垃圾回收时,首先中断所有程序,如果发现没有到安全点,则恢复程序,让其执行到安全点再暂停。目前没有虚拟机使用。
安全区域(Safe Region): 一段代码片段中,对象的引用不会发生任何改变,这个区域的任务位置开始垃圾回收,都是安全的。Java程序中,线程处于Sleep状态或Bloked状态。垃圾回收时,安全区域处理逻辑: 当线程运行到安全区域时,标识进入安全区域。如果此时发生垃圾回收,垃圾回收线程会忽略安全区域标识状态的线程。当线程即将离开安全区域时,会检查垃圾回收是否完成。如果垃圾回收没完成,则等待收到可以离开安全区域的信号为止。
四. HotSpot虚拟机经典算法
串行垃圾回收器: Serial, Serial Old
并行垃圾回收器: ParNew, Parallel Scavenge, Parallel Old
并发垃圾回收器: CMS, G1, ZGC
垃圾回收期组合关系:
1.两个收集器之间有连线,代表它们可以相互配合使用。
Serial/Serial Old, Serial/CMS,
ParNew/Serial Old, ParNew/CMS,
Parallel Scavenge/Serial Old, Parallel Scavenge/Parallel Old,
G1,ZGC
2. 当CMS出现 "Concurrent Mode Failure"失败后,使用Serial Old作为备用方案处理。
3.红色虚线的组合,Serial/CMS 和 ParNew/Serial Old , 在JDK8 时声明为废弃(JEP 173),在JDK9中直接移除支持(JEP214)。
4.绿色虚线的组合, Parallel Scavenge/Serial Old,在JDK14中弃用(JEP 366)。
5.青色虚线组合, 在JDK14删除CMS垃圾回收器(JEP 363)。
调优垃圾回收时,根据实际的业务场景,程序运行状态,选择最合适的垃圾回收器组合。
串行垃圾回收器
Serial : 使用复制算法进行,串行执行垃圾回收。会导致长时间的系统停顿(Stop The World)。年轻代可使用此算法回收垃圾。
Serial Old : 使用标记-压缩算法,串行执行垃圾回收。 老年代使用此算法进行回收垃圾。①与Parallel Scavenge 配合垃圾回收。②作为老年代CMS收集器的后备垃圾收集方案。
在HotSpot 虚拟机中, 使用 -XX:+UseSerialGC 参数,指定年轻代使用 Serial ,老年代使用 Serial Old 。
并行垃圾回收器
ParNew 回收器:主要是并行进行垃圾回收。在年轻代也是使用复制算法进行垃圾回收。
在HotSpot 虚拟机中, 使用 -XX:+UseParNewGC 参数,指定年轻代使用 ParNew 回收器。
Parallel Scavenge 回收器:并行执行,使用复制算法,标记-压缩算法进行垃圾回收。设计主要为了改善吞吐量问题。
在HotSpot 虚拟机中, 使用 -XX:+UseParallelGC 参数,指定年轻代使用 Parallel 并行垃圾回收器。使用 -XX:+UseParallelOldGC 参数,指定老年代使用 Parallel 并行垃圾回收器。
CMS 垃圾回收器:是HotSpot 虚拟机第一次实现垃圾回收器与用户线程一起工作的回收器。CMS回收器是专门给老年代使用的,采用标记-清除算法实现。CMS有并发收集,低延迟特性。但同时会产生内存碎片。对CPU资源敏感,无法处理浮动垃圾。注意,JDK14中移除了CMS垃圾回收器。
在HotSpot 虚拟机中, 使用 -XX:+UseConcMarkSweepGC 参数,指定使用CMS回收器工作。同时会自动打开 -XX:+UseParNewGC。即使用 ParNew(Young区) + CMS(Old区)+ Serial Old(Old区) 的组合进行垃圾回收。
-XX:CMSInitiatingOccupanyFraction 设置内存达到多大阈值,可进行垃圾回收。JDK5 及以前,默认是68。即老年代使用达到68%时,进行垃圾回收。JDK6默认以上的版本是92。
-XX:+UseCMSCompactAtFullCollection 指定执行完Full GC后, 是否对内存空间进行压缩整理,处理内存碎片问题。
-XX:CMSFullGCsBeforeCompaction 设置多少次Full GC后,对内存空间进行压缩整理。
-XX:ParrallelCMSThreads 设置CMS线程的数量。CMS 默认启动线程数是(ParrallelCMSThreads + 3)/4。
工作原理
初始标记阶段(Initial - Mark): 暂停用户线程,搜索GC Roots能直接关联的对象。 然后恢复用户线程执行。
并发标记阶段(Concurrent - Mark): 垃圾回收线程,从GC Roots能直接关联的对象开始遍历所有直接或间接引用的对象,标记为使用对象。同时用户线程回同步执行。
重新标记阶段(Remark): 由于并发标记阶段,用户线程同步执行,会导致部分引用对象标记不准确。这里重新暂停用户程序,重新检查因为用户线程运行而导致标记变动的那一部分对象的标记记录。
并发清除阶段(Concurrent - Sweep): 将标记为垃圾对象进行清除,释放内存空间。同时用户线程同步执行。
记住口令:
最小化内存和并行开销,请选择 Serial GC。最大化应用程序吞吐量,请选择 Pararrel GC。最小化中断和暂停选择CMS GC。
G1(Garbage First) GC,区域分代化, 为了在不断扩大内存和不断增加的处理器数量下,进一步降低暂停时间(Pause time),同时兼顾良好的吞吐量,而重新开发的垃圾回收器。并行并发特性,分代收集功能。可预测的停顿时间模型。 G1设计原则是简化JVM性能调优,开发人员三步即可完整操作。①开启G1垃圾收集器。②设置堆的最大内存。③设置最大的停顿时间。三种垃圾回收模式:YoungGC, Mixed GC 和 Full GC, 在不同的条件下触发。
在HotSpot 虚拟机中, 使用 -XX:+UseG1GC 指定使用G1垃圾回收器。
-XX:G1HeapRegionSize 设置每个Region 大小。具体是2的N次幂。范围在1MB到32MB之间。 默认将整堆划分为2048
个分区
-XX:MaxGCPauseMills 期望GC最大的停顿时间指标。JVM会尽力控制在指标范围内,但不能保证达到。默认是200ms。G1会收集每个Region的回收之后的空间大小、回收需要的时间,根据评估得到的价值,在后台维护一个优先级列表,然后基于我们设置的停顿时间优先回收价值收益最大的Region。
* 全局并发标记(global concurrent marking)
* 拷贝存活对象(evacuation)
1、起初除GC Roots 外的其他对象全部在白色集合,将GC Roots直接引用的对象从白色集合内移到灰色集合。
2、从灰色集合取出一个灰色对象,依次处理该对象引用的对象。若其未引用任何对象,则直接将其移入黑色集合中;若其引用的对象在白色集合中则将其引用对象移入灰色集合,否则直接不处理。当该灰色对象引用的对象全处理完后,再将其移入黑色集合中。
3、重复第2步的流程直到灰色集合为空。
4、上面的步骤处理完后,GC Roots与黑色集合内的对象为存活对象,而白色集合内的对象为垃圾对象,最后要做的就是将白色集合内的垃圾对象清理。
ZGC ():
五.垃圾回收器(GC)日志分析
JVM 开启垃圾回收器日志分析参数列表:
-XX:+PrintGC 输出GC日志。与参数 -verbose:gc 作用一样。
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 以基准时间(例如: 1.782)的形式输出GC的时间戳。1.105: [GC (Allocation Failure) 40309K->9485K(125952K), 0.0059919 secs]
-XX:+PrintGCDateStamps 以日期形式(例如: 2022-05-13T11:11:57.777+0800)输出GC的时间戳。
2022-05-13T11:11:57.777+0800: 0.641: [GC (Allocation Failure) 33280K->5242K(125952K), 0.0045109 secs]
-XX:+PrintHeapAtGC 在进行GC前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输入路径(当前程序运行目录的上层目录下的logs目录,然后创建gc.log 文件)
日志分析:(一):
1.105: [GC (Allocation Failure) 40309K->9485K(125952K), 0.0059919 secs]
1.216: [GC (Metadata GC Threshold) 24014K->11087K(159232K), 0.0062206 secs]
1.226: [Full GC (Metadata GC Threshold) 11087K->6612K(130048K), 0.0265017 secs]
GC, Full GC: GC的类型。其中 GC 是表示在年轻代上进行的垃圾回收。Full GC 表示在 年轻代,老年代,以及永久代(JDK8元空间)上的垃圾回收。
Allocation Failure GC 的原因。表示年轻代没有足够的空间创建新对象,导致垃圾回收。
40309K->9485K 堆垃圾回收前的大小,堆垃圾回收后的大小。
125952K 现在堆的大小。
0.0059919 secs 垃圾回收持续的时间。
日志分析:(二)
2022-05-13T11:23:53.135+0800: 1.126: [GC (Allocation Failure) [PSYoungGen: 38381K->5101K(38400K)] 40321K->9403K(125952K), 0.0062892 secs] [Times: user=0.11 sys=0.00, real=0.02 secs]
2022-05-13T11:23:53.244+0800: 1.227: [GC (Metadata GC Threshold) [PSYoungGen: 19487K->5096K(71680K)] 23790K->10714K(159232K), 0.0058572 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-05-13T11:23:53.244+0800: 1.233: [Full GC (Metadata GC Threshold) [PSYoungGen: 5096K->0K(71680K)] [ParOldGen: 5618K->6376K(56832K)] 10714K->6376K(128512K), [Metaspace: 20519K->20518K(1067008K)], 0.0263378 secs] [Times: user=0.17 sys=0.02, real=0.03 secs]
[PSYoungGen: 38381K->5101K(38400K)] 40321K->9403K(125952K), 0.0062892 secs] PSYoungGen 使用Parallel Scavenge 回收器进行年轻代垃圾回收,堆前后变化。后面40321K->9403K(125952K)是总的堆在垃圾回收前后变化。
Metaspace 元空间垃圾回收前后变化。
[Times: user=0.17 sys=0.02, real=0.03 secs]: user:垃圾回收器花费的所有时间。sys: 花费在等待系统调用或系统事件的时间。 real:垃圾回收从开始到结束的时间。包括其他进程占用时间片的实际时间。
日志分析(三):
{Heap before GC invocations=9 (full 2):
PSYoungGen total 111104K, used 6932K [0x00000000d5f00000, 0x00000000e1480000, 0x0000000100000000)
eden space 99840K, 0% used [0x00000000d5f00000,0x00000000d5f00000,0x00000000dc080000)
from space 11264K, 61% used [0x00000000dc080000,0x00000000dc745030,0x00000000dcb80000)
to space 11264K, 0% used [0x00000000e0980000,0x00000000e0980000,0x00000000e1480000)
ParOldGen total 56832K, used 11501K [0x0000000081c00000, 0x0000000085380000, 0x00000000d5f00000)
object space 56832K, 20% used [0x0000000081c00000,0x000000008273b7e0,0x0000000085380000)
Metaspace used 33563K, capacity 35474K, committed 35496K, reserved 1079296K
class space used 4398K, capacity 4757K, committed 4776K, reserved 1048576K
2022-05-13T11:23:55.150+0800: 3.133: [Full GC (Metadata GC Threshold) [PSYoungGen: 6932K->0K(111104K)] [ParOldGen: 11501K->11054K(83456K)] 18434K->11054K(194560K), [Metaspace: 33563K->33563K(1079296K)], 0.0575905 secs] [Times: user=0.25 sys=0.02, real=0.06 secs]
Heap after GC invocations=9 (full 2):
PSYoungGen total 111104K, used 0K [0x00000000d5f00000, 0x00000000e1480000, 0x0000000100000000)
eden space 99840K, 0% used [0x00000000d5f00000,0x00000000d5f00000,0x00000000dc080000)
from space 11264K, 0% used [0x00000000dc080000,0x00000000dc080000,0x00000000dcb80000)
to space 11264K, 0% used [0x00000000e0980000,0x00000000e0980000,0x00000000e1480000)
ParOldGen total 83456K, used 11054K [0x0000000081c00000, 0x0000000086d80000, 0x00000000d5f00000)
object space 83456K, 13% used [0x0000000081c00000,0x00000000826cb920,0x0000000086d80000)
Metaspace used 33563K, capacity 35474K, committed 35496K, reserved 1079296K
class space used 4398K, capacity 4757K, committed 4776K, reserved 1048576K
}
常见日志分析工具:
GCViewer
GCEasy 第三方JC分析网站。https://gceasy.io/
GCHisto
GCLogViewer
Hpjmeter
garbagecat
六.垃圾回收器调优实战分析
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通