垃圾回收介绍
目录
一、为什么需要垃圾回收
二、GC回收区域
1、堆内存
2、堆内存按对象存活特征分为新生代和老年代,两者的回收策略完全不同。
三、标记过程
1、引用计数算法
(1)内存消耗的更多
(2)可能出现“循环引用”问题
2、可达性分析算法
(1)基本思路:
(2)过程图示:
四、回收过程
1、标记—清除算法
2、 复制算法
3、标记—整理算法
4、分代算法
(1)核心思想:
(2)依据:
(3)新生代
(4)老年代
五、垃圾回收器具体实现
1、 Serial GC(新生代收集器,串行GC)
2、ParNew GC(新生代收集器,并行GC)
3、Parallel Scavenge收集器(新生代收集器,并行GC)
4、Serial Old收集器(老年代收集器,串行GC)
(1)Client模式
(2)Server模式
5、Parallel Old收集器(老年代收集器,并行GC)
6、CMS收集器(老年代收集器,并发GC)
在程序运行过程中,内存管理是至关重要的环节。如果开发者手动管理内存,稍有不慎就可能出现内存泄露、野指针等问题,严重影响程序的稳定性。而垃圾回收机制的出现,为我们解决了这些难题。下面,我们就来总结一下垃圾回收的核心知识点。
一、为什么需要垃圾回收
早期编程语言(如 C、C++)中,内存的分配与释放需要开发者手动完成。这要求开发者对内存管理有深入的理解,否则很容易出现问题。
比如,当一块内存不再被使用时,如果开发者忘记释放,就会造成内存泄露,随着程序运行时间的增长,可用内存会越来越少,最终可能导致程序崩溃;而如果释放了仍在使用的内存,又会出现野指针问题,引发程序运行时错误。
垃圾回收(Garbage Collection,GC)的出现就是为了自动管理内存,它能自动识别并回收不再被使用的内存空间。开发者无需手动释放内存,降低内存管理的复杂度,减少了因人为操作失误导致的内存相关问题,提高程序的安全性和开发效率。同时垃圾回收可以更合理地利用内存资源,让程序在运行过程中能更稳定地获取所需内存。
二、GC回收区域
1、堆内存
在 Java 虚拟机(JVM)的内存布局中,垃圾回收主要针对的是堆内存。
堆内存是用来存储对象实例的,而对象的创建和销毁非常频繁,且对象是否还被使用难以通过手动方式准确判断,非常适合由垃圾回收机制进行管理。
2、堆内存按对象存活特征分为新生代和老年代,两者的回收策略完全不同。
后面讲回收算法的时候会详细来说。
三、标记过程
1、引用计数算法
给对象增加⼀个引用计数器,每当有⼀个地⽅引⽤它时,计数器就+1;
当引⽤失效时,计数器就-1;
任何时刻计数器为0的对象就是不能再被使⽤的,即对象已"死"。
每个对象在new的时候,都会搭配一个小的内存空间保存一个整数,即引用计数。
引用计数可能会出现下面的问题:
(1)内存消耗的更多
如果对象本身比较小的话,引用计数消耗的空间比列就比较大
例如,一个 int 类型的对象本身占用 4 字节,若额外添加 4 字节计数器,内存消耗直接翻倍;而在堆内存中,小对象的占比往往很高,累积的额外内存消耗会非常显著。
(2)可能出现“循环引用”问题
循环引用指两个或多个对象之间互相持有引用,形成闭环。
class A {B b;
}
class B {A a;
}// 创建循环引用
A objA = new A();
B objB = new B();
objA.b = objB; // A引用B
objB.a = objA; // B引用A// 释放外部引用
objA = null;
objB = null;
此时,objA和objB的外部引用已被清空,但它们内部互相引用(A.b指向B,B.a指向A),形成了 “A→B→A” 的闭环。
初始状态:objA和objB的计数器均为 1(被外部变量引用)。
建立内部引用后:objA的计数器因objB.a引用变为 2;objB的计数器因objA.b引用变为 2。
释放外部引用后:objA和objB的计数器各自减 1,最终均为 1(因互相引用)。
由于计数器始终不为 0,引用计数算法会判定这两个对象 “仍在被使用”,不会回收它们。但实际上,这两个对象已没有任何外部引用,永远无法被访问,却会一直占用内存 —— 这就是循环引用导致的内存泄露。
2、可达性分析算法
由于引用计数算法无法解决循环引用的问题,因此主流JVM 选择可达性分析算法
(1)基本思路:
以一系列称为 “GC Roots” 的对象作为起点,从这些起点开始向下搜索,搜索过程中所走过的路径称为 “引用链”。如果一个对象能通过 GC Roots 的引用链到达,那么这个对象就是存活的;反之,如果一个对象没有任何引用链连接到 GC Roots,即从 GC Roots 出发无法访问到该对象,那么这个对象就被判定为死亡对象,可被回收。
可作为GC Roots对象的有:
栈上的局部变量(引用类型);
常量池引用指向的对象;
静态成员(引用类型)
(2)过程图示:
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为 可回收对象 。
可达性分析的过程就如同树/图的遍历
四、回收过程
在完成标记过程,确定了可回收的对象后,就进入了回收阶段。不同的垃圾回收算法,回收过程有所不同,常见的回收算法有以下几种:
1、标记—清除算法
标记 - 清除算法是最基础的垃圾回收算法,它分为 “标记” 和 “清除” 两个阶段。
(1)标记阶段:使用可达性分析算法标记出所有需要回收的对象。
(2)清除阶段:直接回收所有被标记的对象所占用的内存空间,释放出的内存空间会被放入空闲列表中,以便后续对象分配内存时使用。
不过,这种算法存在两个明显的缺点:
一是标记和清除过程效率不高,尤其是当堆内存中对象数量较多时;
二是会产生大量不连续的内存碎片,内存碎片如果太多的话,总的空闲空间虽然很大,但是想要申请一个稍微大一点的内存空间都会失败
2、 复制算法
为了解决标记 - 清除算法的内存碎片问题,复制算法应运而生。它将堆内存划分为大小相等的两块,每次只使用其中一块。
当使用的这块内存快满时,就会触发垃圾回收。首先标记出存活的对象,然后将这些存活对象复制到另一块未使用的内存区域中,并且按顺序排列,避免了内存碎片。
之后,将原来使用的那块内存区域中的所有对象(包括可回收对象)全部清除。
复制算法优缺点:
(1)优点:实现简单、运行高效,且不会产生内存碎片。
(2)缺点:需要将内存划分为两块,导致实际可用内存减少了一半,内存利用率较低;一旦不是垃圾的对象比较多,这种复制的成本就比较大。
3、标记—整理算法
标记 - 整理算法结合了标记 - 清除算法和复制算法的优点,主要用于对象存活率较高的场景。
(1)标记阶段:标记阶段和标记 - 清除算法一样,标记出所有存活的对象。
(2)清楚阶段:将所有存活的对象向内存的一端移动,然后直接清除掉边界以外的所有内存空间。
但标记 - 整理算法在整理阶段需要移动存活对象,这会增加额外的开销,效率相对较低。
4、分代算法
分代算法结合了标记 - 清除、复制、标记 - 整理等基础算法,针对不同生命周期的对象进行差异化管理 。
(1)核心思想:
分代算法的思想就好比中国的⼀国两制方针⼀样,对于不同的情况和地域设置更符合当地的规则,从而实现更好的管理。
(2)依据:
对象的存活时间存在显著差异
“大多数对象存活时间很短,少数对象存活时间很长”。具体来说,新创建的对象往往在经历几次垃圾回收后就会被回收,而存活下来的对象则可能在较长时间内持续被使用。
基于这个特征,分代算法将堆内存划分为新生代和老年代。
新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采⽤复制算法;
而⽼年代中对象存活率⾼、没有额外空间对它进⾏分配担保,就必须采⽤"标记-清理"或者"标记-整理"算法。
(3)新生代
用于存储新创建的对象,是垃圾回收最频繁的区域。根据对象的存活情况,年轻代又进一步划分为 Eden(伊甸) 区和两个大小相等的 Survivor(幸存) 区(通常称为 From 区和 To 区)。
Eden 区:新对象优先在 Eden 区分配内存,大多数对象在这里被创建,也在这里被回收。
Survivor 区:用于存储在 Eden 区垃圾回收后存活下来的对象。两个 Survivor 区在回收过程中会交替使用,每次只有一个 Survivor 区被使用,另一个作为备用。
新生代中的对象大部分会快速消亡,使每次复制的开销都可控。
Minor GC(新生代回收):
因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采⽤复制算法)非常频繁,⼀般回收速度也比较快。
1、当 Eden 区的内存不足时,触发 Minor GC。
2、首先标记出 Eden 区和正在使用的 Survivor 区(From 区)中存活的对象。
3、将存活的对象复制到另一个未使用的 Survivor 区(To 区),同时给这些对象的 “年龄计数器” 加 1。
4、清空 Eden 区和 From 区的内存,此时 From 区和 To 区的角色互换(原来的 To 区变为下次回收时的 From 区)。
5、如果存活对象的年龄计数器达到阈值(如 15),则将其转移到老年代;如果 Survivor 区中存活对象的大小超过该区的一半,也会直接进入老年代(避免 Survivor 区频繁溢出)。
(4)老年代
老年代用于存储从年轻代存活下来的对象。当年轻代的对象经历一定次数的垃圾回收后仍然存活(默认是 15 次,可通过参数调整),就会被转移到老年代。
一些大对象可能会直接在老年代分配(避免在年轻代中频繁复制)。
老年代的对象存活时间长、存活率高,垃圾回收的频率相对较低。
Major GC/Full GC(老年代回收)
老年代的对象存活率高,且没有额外的备用内存区域,因此通常采用标记 - 整理算法进行回收,Major GC的速度⼀般会比Minor GC慢10倍以上。流程如下:
1、当老年代的内存不足,或满足其他触发条件(如年轻代对象无法进入老年代)时,触发 Major GC(若同时回收年轻代和老年代,则称为 Full GC)。
2、标记阶段:从 GC Roots 出发,标记出老年代中所有存活的对象。
3、整理阶段:将存活的对象向老年代的一端移动,然后清除边界以外的所有内存空间,避免产生内存碎片。
五、垃圾回收器具体实现
垃圾回收器是垃圾回收算法的具体实现,不同的垃圾回收器针对不同的场景进行了优化,以满足不同的性能需求。以下是几种典型的垃圾回收器:
1、 Serial GC(新生代收集器,串行GC)
Serial GC 是最基本、最简单的垃圾回收器。它在进行垃圾回收时,会暂停所有用户线程(即 “Stop-The-World”,STW),使用单线程执行垃圾回收操作。
优点:实现简单、占用内存少,适合在单核 CPU、内存较小的环境中使用,比如嵌入式设备。
缺点:在垃圾回收时会暂停所有用户线程,且单线程执行回收,在堆内存较大或多线程应用中,会导致较长的停顿时间,影响程序的响应速度,因此不适合高性能应用。
应用场景:Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
2、ParNew GC(新生代收集器,并行GC)
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进⾏垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全⼀样,在实现上,这两种收集器也共⽤了相当多的代码。
应用场景:ParNew收集器是许多运⾏在Server模式下的虚拟机中⾸选的新⽣代收集器。
与Serial对比:
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
然而,随着可以使⽤的CPU的数量的增加,它对于GC时系统资源的有效利⽤还是很有好处的。
,
3、Parallel Scavenge收集器(新生代收集器,并行GC)
Parallel Scavenge收集器是⼀个新⽣代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器使用两个参数控制吞吐量:
MaxGCPauseMillis 控制最大的垃圾收集停顿时间;
GCRatio 直接设置吞吐量的大小
应用场景:
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
自适应调节策略:
Parallel Scavenge收集器有⼀个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就不需要手工指定新⽣代的大小、Eden与Survivor区的比例、晋升⽼年代对象年龄等细节参数了,虚拟机会根据当前系统的运⾏情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
4、Serial Old收集器(老年代收集器,串行GC)
Serial Old是Serial收集器的老年代版本,它同样是⼀个单线程收集器,使用标记−整理算法。
应用场景:
(1)Client模式
SerialOld收集器的主要意义也是在于给Client模式下的虚拟机使⽤。
(2)Server模式
在JDK 1.5以及之前的版本中与 Parallel Scavenge收集器搭配使⽤;
还可以作为CMS收集器的后备预案,在并发收集发生Concurrent ModeFailure时使⽤。
5、Parallel Old收集器(老年代收集器,并行GC)
Parallel Old是Parallel Scavenge收集器的老年代版本,使⽤多线程和“标记−整理”算法。
应用场景:
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
6、CMS收集器(老年代收集器,并发GC)
CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前⾯⼏种收集器来说更复杂⼀
些,整个过程分为4个步骤:
(1)初始标记(CMS initial mark) 初始标记仅仅只是标记⼀下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
(2)并发标记(CMS concurrent mark)并发标记阶段就是进行GC Roots Tracing的过程。
(3)重新标记(CMS remark) 重新标记阶段是为了修正并发标记期间因用户程序继续运作⽽导致标记产⽣变动的那⼀部分对象的 标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段稍⻓⼀些,但远⽐并发标记的时间短,仍然 需要“Stop The World”。
(4)并发清除(CMS concurrent sweep)并发清除阶段会清除对象。
CMS收集器的内存回收过程是与用户线程⼀起并发执⾏的。
优点:CMS是⼀款优秀的收集器,他的主要优点是并发收集、低停顿。
缺点:CMS收集器对CPU资源⾮常敏感;CMS收集器⽆法处理浮动垃圾;CMS收集器会产⽣⼤量空间碎片;