一、垃圾回收概述
(1)垃圾回收主要解决的问题
- 内存溢出:当程序在运行过程中,所需的内存超出了 JVM 被分配到的内存空间时,就会发生内存溢出。垃圾回收会将不再被引用的对象进行回收,释放内存空间,以避免或缓解内存溢出问题
- 内存泄漏:内存泄漏指对象已经不再被使用,但由于某些原因(如持有无用对象的引用),仍然无法被垃圾回收机制回收。垃圾回收机制会尽量识别并解决这种情况,不过一些复杂的内存泄漏场景可能需要开发者手动排查和修复
(2)触发垃圾回收的情况
- 被动触发:
- 当伊甸园区内存空间已满时,会触发 Minor GC,主要回收新生代的垃圾对象
- 当老年代内存空间已满时,通常会触发 Major GC ,在很多情况下,Major GC 会伴随着 Full GC,Full GC 会对整个堆(包括新生代、老年代和方法区等)进行垃圾回收
- 当堆内存的总占用达到 JVM 设置的某个阈值时,也会触发垃圾回收
- 主动触发:程序中调用 System.gc() 或者 Runtime.getRuntime().gc() 方法,这只是向 JVM 发出进行垃圾回收的建议,JVM 并不一定会立即执行垃圾回收操作,它会根据自身的状态和策略来决定是否进行回收
二、垃圾回收相关算法
- 标记 - 清除算法:先标记出垃圾对象,之后统一清除。此方法会产生大量内存碎片,这些碎片难以被有效利用。比如需要存放一个大对象时,分散的内存碎片可能无法提供足够的连续空间(可联想到对象分配内存时的空闲列表法)
- 复制算法:将存活对象从一块内存空间复制到另一块。该算法适用于存活对象较少的场景,复制操作较少,效率较高。但缺点是需要额外的内存空间,实际可用内存变为原来的一半
- 标记 - 整理算法:先标记出垃圾对象,然后把存活对象整理到内存的一侧,接着清除另一侧的垃圾对象。这样解决了标记 - 清除算法产生大量内存碎片的问题,使内存布局更加规整(可联想到对象分配内存时的指针碰撞法)
- 分代回收算法:针对新生代和老年代的不同特点,分别采用不同的垃圾回收算法。新生代中对象存活率较低,采用复制算法效率较高;老年代中对象存活率较高,一般采用标记 - 整理算法,避免内存碎片对后续大对象分配的影响
三、垃圾器 CMS 和 G1的区别
- 回收范围:
- CMS 垃圾回收器主要针对老年代进行垃圾回收,它需要和新生代垃圾回收器(如 Serial 或 ParNew)配合使用,才能实现全堆的垃圾清理
- 而 G1 垃圾回收器打破了传统的新生代和老年代的划分,将堆内存划分为多个 Region,可以独立完成全堆的垃圾回收工作
- 算法使用及内存碎片问题
- CMS 基于标记 - 清除算法,该算法在回收垃圾后容易产生内存碎片
- G1 则更为灵活,对于新生代的 Region 采用复制算法,对于老年代的 Region 采用标记 - 整理算法。复制算法和标记 - 整理算法在处理存活对象时,能有效避免或减少内存碎片的产生。不过严格来说,复制算法和标记 - 整理算法在极端情况下也可能存在少量碎片,但相比标记 - 清除算法要好很多
- 内存整理情况
- CMS 使用标记 - 清除算法,只是简单标记并清除垃圾对象,不会对内存进行整理,从而导致内存碎片。G1 使用的复制算法是将存活对象复制到新的区域,标记 - 整理算法会将存活对象移动到内存的一端,这两种算法都在回收垃圾的同时对内存进行了整理,使内存布局更加规整
- 停顿时间控制:CMS 的设计目标是尽可能缩短垃圾回收时的停顿时间,提高应用的响应性能。但由于每次回收时垃圾的分布和数量难以精确预估,导致停顿时间的波动较大。G1 引入了预测模型,允许用户手动设置最大停顿时间目标。G1 会根据这个目标,优先选择垃圾回收收益最大(即垃圾数量最多)的 Region 进行回收,从而更精准地控制停顿时间
- 浮动垃圾问题:CMS 在并发标记和并发清除阶段,由于应用程序线程和垃圾回收线程同时运行,会产生新的垃圾,这些垃圾被称为浮动垃圾,只能留到下一次垃圾回收时处理。G1 在筛选回收阶段会发生 Stop - The - World,在这个阶段应用程序线程暂停,不会产生浮动垃圾,但在并发标记阶段同样可能产生少量浮动垃圾
四、垃圾回收器
- Serial:它是单线程的垃圾回收器,在进行垃圾回收时,JVM 只会启动一个 GC 线程,并且在回收过程中,应用程序的所有线程都要暂停(Stop - The - World)。由于单线程执行垃圾回收工作,处理任务量较大,因此 Stop - The - World 的时间相对较长。它主要适用于单 CPU 环境下的小型应用程序
- CMS(Concurrent Mark Sweep Collector):这是一款多线程并发的垃圾回收器,JVM 会启动多个 GC 线程并行进行垃圾回收工作。多个线程同时工作使得回收速度加快,能有效缩短 Stop - The - World 的时间,提升应用程序的响应性能。不过,它基于 “标记 - 清除” 算法,在回收后会产生大量内存碎片。CMS 主要用于老年代的垃圾回收,通常需要和 Serial 或 ParNew 等新生代垃圾回收器配合使用
- G1(Garbage - First):G1 是一款面向服务端应用的垃圾回收器,它打破了传统的新生代和老年代的划分方式,将堆内存划分为多个大小相等的独立区域(Region)。G1 可以同时回收新生代和老年代,它会根据每个 Region 中垃圾的数量,优先回收垃圾最多的区域(即 “Garbage - First”)。G1 采用多线程并行回收的方式,在不同的 Region 会根据对象存活情况采用不同的算法,比如在新生代的 Region 采用复制算法,老年代的 Region 采用标记 - 整理算法,以此来避免内存碎片问题
- ZGC(Z Garbage Collector):ZGC 是一种可伸缩的、低延迟的垃圾回收器,适用于堆内存非常大(从几百 MB 到数 TB)的场景。它的设计目标是在尽可能不影响应用程序性能的前提下,高效地回收大量数据,其停顿时间可以控制在 10 毫秒以内,即使在处理海量数据时也能保证极低的延迟
五、如何判断谁是垃圾?
(1)判断垃圾的方法
- 引用计数法:对象每被引用一次,其对应的引用计数器就加 1;每次引用被撤销,引用计数器就减 1。当引用计数器的值为 0 时,该对象就可以被回收。不过,这种方法存在缺陷,例如当 A 对象持有 B 对象的引用,同时 B 对象持有 A 对象的引用,即便它们都不再被其他对象使用,由于双方引用计数器的值都不为 0,这两个对象都无法被垃圾回收,出现类似于 “死锁” 的循环引用情况
可达性分析法:从 GC Roots 开始,通过引用链标记可达对象。可达对象不能被回收,不可达对象则可以被回收
(2)GC Roots
- 虚拟机栈中栈帧的局部变量表中的变量:这些变量引用的对象在方法执行期间是可达的
- 方法区中的静态变量:静态变量属于类,在类加载时就存在,它们引用的对象也是可达对象
- 方法区中的常量:引用类型的常量指向堆中的对象,是可达对象的一部分
- 本地方法栈中栈帧的局部变量表中的变量:本地方法栈用于执行本地方法,其中局部变量表中的变量引用的对象同样为可达对象
六、GC的类型
- Minor GC:当伊甸园区空间不足时触发,主要对整个新生代进行垃圾回收。默认情况下,新生代与老年代的空间占比约为 1 : 2 ,新生代空间相对较小。并且新创建的对象大多会存放在新生代的伊甸园区,所以 Minor GC 触发较为频繁。不过由于清理的区域相对较小,且新生代中大部分对象的生命周期较短,很多对象在第一次 Minor GC 时就会被回收,因此 Minor GC 的清理速度通常较快
- Major GC:一般是当老年代空间不足时触发,它主要回收老年代的垃圾对象。虽然部分情况下 Major GC 可能会伴随着 Minor GC 一起执行,但它并不一定会对新生代进行回收。老年代空间较大,且其中存活对象较多,垃圾回收的难度和工作量相对较大,所以清理速度较慢,触发频率也相对较低
- Full GC:触发条件包括方法区内存不足、堆内存达到一定阈值,或者手动调用 System.gc()、Runtime.getRuntime().gc() 方法等。Full GC 会对整个堆内存(包括新生代和老年代)以及方法区进行垃圾回收。由于其清理的范围最广、涉及的对象数量最多,所以清理速度最慢,触发频率也最低
三者的清理速度排序通常为:Minor GC>Major GC>Full GC
七、G1 垃圾回收过程
- 初始标记阶段:触发 G1 垃圾回收时,JVM 开启多个线程进行回收工作。此阶段会发生 Stop - The - World,暂停所有正在运行的 Java 线程,目的是标记出所有从 GC Roots 直接可达的对象。这个阶段的停顿时间较短
- 并发标记阶段:Java 线程恢复运行,与此同时,垃圾回收线程也在后台并发执行。它们会从初始标记阶段标记的对象开始,遍历整个堆,找出所有可达对象,并对对象的引用关系进行标记。该阶段不会影响应用程序的正常运行
- 最终标记阶段(再标记阶段):再次触发 Stop - The - World。在并发标记阶段,由于 Java 线程仍在运行,对象的引用关系可能发生了变化,所以这个阶段会对这些变化进行修正。它会使用 SATB(Snapshot At The Beginning)算法来快速完成检查,而不用扫描整个堆,因此暂停时间相对较短
- 筛选回收阶段:第三次触发 Stop - The - World。在这个阶段,垃圾回收线程会根据各个 Region 的垃圾回收价值(即垃圾数量)进行排序,优先选择垃圾最多的 Region 进行回收。同时,会记录哪些 Region 中有存活对象,哪些 Region 是空的。这一阶段会根据用户设定的停顿时间目标,制定回收计划,尽可能在规定时间内完成垃圾回收工作
- 存活对象处理(转移阶段):垃圾回收线程会对存活对象进行处理。对于属于新生代的 Region,采用复制算法,将存活对象复制到新的空闲 Region 中;对于属于老年代的 Region,采用标记 - 整理算法,把存活对象移动到内存的一端,以消除内存碎片