JVM知识点-垃圾回收算法
注:更多文章,可以进入我的博客:筱白博客
1、什么是垃圾?
程序中的一块内存没有被任何变量持有引用,导致这块内存无法被这个程序再次访问时,这块内存被称为垃圾。
例如:在 Java 程序中,每 new 一个对象,就会在栈或堆中分配一块内存,比如这一行代码:
Object o = new Object();
变量 o 保存了这个对象的内存地址,我们称之为 o 持有这个 new Object() 的引用,当 o 被置为 null 时:
o = null;
在栈或堆中,这个 new Object() 分配后的内存不再被任何变量引用,它就成为了一个垃圾。
2、怎么找到垃圾?
① 引用计数法
每个对象有一个引用计数属性,每多一个引用指向这个对象,计数 + 1,每少一个引用指向这个对象,计数 -1,当计数为 0 时,表示这个对象成为了一个垃圾,将其回收掉。
此方法简单,但是无法解决对象相互循环引用的问题,例如:
public class Client {
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.o = b;
b.o = a;
}
}
class Test {
Object o;
}
在这种情况下,a 引用了 b,b 又引用了 a。如果使用引用计数法,则它们的计数都为 1。当 main 方法执行完后,a 和 b 都不再被使用。但由于它们的引用计数不为 0,导致它们将无法被 GC 回收掉,所以 Java 中并没有采用引用计数法来进行内存回收。
② 可达性分析法
可达性分析法又被称为根搜索算法,这个算法解决了循环引用的问题。GC 定义了一些根(roots),从GC Roots开始向下搜索,搜索所走过的路径称为**引用链。**当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可达对象,即表示为垃圾。
GC roots 包括:虚拟机栈(局部变量表)中引用的对象、方法区中静态引用的对象、存活的线程对象等等。
3、垃圾回收算法
① 标记-清除算法
分为两个阶段,标记阶段和清除阶段。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
优点:算法简单,容易理解。
缺点:容易产生内存碎片,可能导致最后无法找到一块连续的内存存放大对象。
② 复制算法
为了解决标记-清除算法产生的内存碎片化的问题。该算法按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
优点:不会有内存碎片。
缺点:
- 浪费空间,可用内存减少。
- 移动时,需要复制对象,并调整对象引用。
- 存活对象多的话,该算法效率会大大降低。
③ 标记-压缩算法
标记阶段和标记-清除算法相同,标记后不是清理对象,而是将存活的对象移向内存的一端,然后清除端边界外的对象。
优点:
- 不会有内存碎片。
- 不会使内存减少。
缺点:移动时,需要复制对象,并调整对象引用。
④ 分代收集算法
分代收集法是目前大部分JVM所采用的方法,把Java堆分为新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法。
一般而言,新生代是采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,所以复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的伊甸区和两个较小的存活区(比例8:1:1),每次使用伊甸区和其中的一块存活区,当进行回收时,将该两块已使用的区域中还存活的对象复制到另一块的存活区中。
而老年代因为每次只回收少量对象,因而采用标记-清除算法或者标记-压缩算法。
4、内存分代模型
内存分代模型将堆内存中的区域分成两部分:新生代和老年代。两块区域的比例默认是1:2。
(1)
新生代又分为一个伊甸区(Eden),两个存活区(survivor)。当对象刚 new 出来时,通常分配在伊甸区,伊甸区的对象大多数生命周期都比较短,据不完全统计,每次 GC 时,伊甸区存活对象只占 5% - 10%,由于存活对象较少,所以新生代的垃圾回收算法采用的是 复制算法,但这里的复制算法并不是将新生代一分为二,因为伊甸区存活的对象数量较少,所以存活区只需要较小的内存(伊甸区和存活区的默认比例是 8:1:1)。
新生代的 GC 被称之为 YGC(Young Garbage Collector,年轻代垃圾回收)或者 MinorGC(Minor Garbage Collector,次要垃圾回收),整个回收过程类似这样:
- 对象在伊甸区中被创建出来
- 伊甸区经过一次 GC 之后,存活的对象到达存活 1 区,清空伊甸区
- 伊甸区和存活 1 区的对象经历第二次 GC,存活的对象到达存活 2 区,清空伊甸区和存活 1 区
- 伊甸区和存活 2 区经历第三次回收,存活的对象到达存活 1 区,清空伊甸区和存活 2 区
- 循环往复…
- 每经过一次 GC,没被回收掉的对象年龄 + 1。当存活的对象到达一定年龄(一般为15岁)之后,新生代的对象将会到达老年代。
(2)
老年代的垃圾回收算法采用的是 标记-清除 或者 标记-压缩,因为老年代的空间较大,所以老年代的 GC 并不像新生代那样频繁。
老年代的 GC 称之为 FGC(Full Garbage Collector,完整垃圾回收)或者 MajorGC (Major Garbage Collector,重要垃圾回收)。**YGC/MinorGC 在新生代空间耗尽时触发,FGC/MajorGC 在老年代空间耗尽时触发。**FGC/MajorGC 触发时,新生代和老年代会同时进行 GC。在 Java 程序中,也可以通过 System.gc() 来手动调用 FGC。
(3)
Java 中的整个 垃圾回收(GC) 过程。
- 当对象刚创建时,优先考虑在栈上分配内存。因为栈上分配内存效率很高,当栈帧从虚拟机栈 pop 出去时,对象就被回收了。但在栈上分配内存时,必须保证此对象不会被其他栈帧所引用,否则此栈帧被 pop 出去时,就会出现对象逃逸,产生 bug。
- 如果此对象不能在栈上分配内存,则判断此对象是否是大对象,如果对象过大,则直接分配到老年代。
- 否则考虑在 TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区)上分配内存,这块内存是伊甸区为每个线程分配的一块区域,它的大小是伊甸区的 1% ,作用是减少各个线程创建对象时并发操作争抢伊甸区空间的问题。
- 伊甸区的对象经过 GC,存活的对象在 Survivor 1 区和 Survivor 2 区不断拷贝,到达一定年龄后到达老年代。
- 老年代的垃圾在 FGC 时被回收。
5、空间分配担保机制
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
原理图:
新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
6、垃圾回收器
从上图可以看出:
- 新生代收集器:Serial、 ParNew 、 Parallel Scavenge
- 老年代收集器: CMS 、Serial Old、Parallel Old
- 整堆收集器: G1 , ZGC (因为不涉年代不在图中)。
① Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
② ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核 CPU环境下有着比Serial更好的表现;
③ Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞 吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽 快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
④ Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
⑤ Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收 集器的老年代版本;
⑥ CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回 收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
⑦ G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一 个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收 集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年 代),而前六种收集器回收的范围仅限于新生代或老年代。
⑧ ZGC (Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收 集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内 存多重映射等技术来实现可并发的标记-整理算法的收集器。在 JDK 11 新加入,还在实验阶 段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。优点:低停顿,高吞吐 量, ZGC 收集过程中额外耗费的内存小。缺点:浮动垃圾 目前使用的非常少,真正普及还是需要写时间的。
7、Java中的四种引用类型
Java四种引用包括强引用,软引用,弱引用,虚引用。
① 强引用
只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();
//可直接通过obj取得对应的对象 如obj.equels(new Object());
而这样obj对象对后面new Object()的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
② 软引用
非必须引用,内存溢出之前进行回收,可以通过以下代码实现
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
③ 弱引用
第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
④ 虚引用
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
虚引用主要用于检测对象是否已经从内存中删除。