参考资料:
https://shuyi.tech/archives/jvm-serial-08-jvm-garbage-collection
https://zhuanlan.zhihu.com/p/112018694
判断哪些是垃圾
现今 Java 虚拟机判断垃圾对象使用的是:GC Root Tracing 算法(根搜索算法)。其大概的过程是这样:从 GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾。
通过一系列名为”GCRoots”的对象作为起始点,从这个节点向下搜索,搜索走过的路径称为ReferenceChain,当一个对象到GCRoots没有任何ReferenceChain相连时,则证明这个对象不可用。
其实 GC Root 就是一组活跃引用的集合,这些集合是经过特意筛选出来的,通常包括: - 所有当前被加载的 Java 类 - Java 类的引用类型静态变量 - Java类的运行时常量池里的引用类型常量 - VM的一些静态数据结构里指向GC堆里的对象的引用 - 等等 简单地说,GC Root 就是经过精心挑选的一组活跃引用,这些引用是肯定存活的。那么通过这些引用延伸到的对象,自然也是存活的。
垃圾回收算法
垃圾回收算法简单地说有三种算法:标记清除算法、复制算法、标记整理算法。
标记清除法
在标记阶段,标记所有由 GC Root 触发的可达对象,所有未被标记的对象就是垃圾对象。之后在清除阶段,清除所有未被标记的对象。
标记清除优势
- 标记清除法的特点就是简单直接,速度也非常快,适合存活对象多,需要回收的对象少的场景。
标记清除不足
-
1、会造成不连续的内存空间:就像上图清除后的内存区域一样,清除后内存会有很多不连续的空间,这也就是我们常说的空间碎片,这样的空间碎片太多不仅不利于我们下次分配,而且当有大对象创建的时候,我们明明有可以容纳的总空间,但是空间都不是连续的造成对象无法分配,从而不得不提前触发GC。
-
2、性能不稳定:内存中需要回收的对象,当内存中大量对象都是需要回收的时候,通常这些对象可能比较分散,所以清除的过程会比较耗时,这个时候清理的速度就会比较慢了。
标记复制法
标记清除法最大问题是会造成空间碎片,同时可回收对象如果太多也会影响其性能,而标记复制法则可以解决这两大问题。标记清除法的关注点在可回收的对象身上,而标记复制法的关注点则放在了存活的对象身上,通过把存活的对象转移到一个新的区域,然后对原区域的对象进行统一清理。
首先它把内存划分出三块区域,一块用于存放新创建的对象叫Eden区,另外两块则用于存放存活的对象分别叫 S1区和S2区。回收的时候会有两种情况,一种是把Eden和S1区的存活对象复制到S2区,第二种是把Eden和S2区的存活对象复制到S1区 ,也就是说S1区和S2这两块区域同时只会有一块使用,通过这种方式保证始终会有一块空白的q区域用于下次GC时存放存活的对象,而且原来的区域不需要考虑保留存活的对象,所以可以直接一次性清除所有对象,这要既简单直接同时也保证了清除的内存区域的内存连续性。
标记复制法的优势:
标记复制法解决了标记清除法的空间碎片问题,并且采用移动存活对象的方式,每次清除针对的都是一整块内存,所以清除可回收对象的效率也比较高,但因为要移动对象所以这里会耗费一部分时间,所以标记复制法效率还是会低于标记清除法。
标记复制法的不足:
-
1、会浪费一部分空间:通过上面的图我们也不难发现,总是会有一块空闲的内存区域是利用不到的,这也造成了资源的浪费。
-
2、存活对象多会非常耗时:因为复制移动对象的过程是比较耗时的,这个不仅需要移动对象本身,还需要修改使用了这些对象的引用地址,所以当存活对象多的场景会非常耗时,所以标记复制法比较适合存活对象较少的场景。
-
3、需要担保机制:因为复制区总会有一块空间的浪费,而为了减少浪费空间太多,所以我们会把复制区的空间分配控制在很小的区间,但是空间太小又会产生一个问题,就是在存活的对象比较多的时候,这时复制区的空间可能不够容纳这些对象,这时就需要借一些空间来保证容纳这些对象,这种从其他地方借内存的方式我们称它为担保机制。
标记整理法
标记整理法分为标记和整理两个阶段,标记阶段会先把存活的对象和可回收的对象标记出来;标记完再对内存对象进行整理,这个阶段会把存活的对象往内存的一端移动,移动完对象后再清除存活对象边界之外的对象。
标记整理法的优势:
- 标记整理法是解决了标记复制法浪费空间、不适合存活对象多场景的短板,又解决了标记清除法空间碎片的短板, 所以对于标记复制法不适合的场景,同时又不能忍受标记清除法的空间碎片问题,就可以考虑标记整理法。
标记整理法的不足:
- 没有任何一种算法是万能的,标记整理法看似解决了很多问题,但它本身存在很严重的性能问题,标记整理法是三种垃圾回收算法中性能最低的一种,因为标记整理法在移动对象的时候不仅需要移动对象,还要额外的维护对象的引用的地址,这个过程可能要对内存经过几次的扫描定位才能完成,同时还有清除对象的空座,既然做的事情这么多那么必然消耗的时间也越多。
垃圾回收算法的适用场景
1、标记清除法
特点: 简单、收集速度快,但会有空间碎片,空间碎片会导致后面的GC频率增加。
适合场景:只有小部分对象需要进行回收的,所以标记清除法比较适用于老年代的垃圾回收,因为老年代一般存活对象会比回收对象要多。
2、标记复制法
特点:收集速度快,可以避免空间碎片,但是有空间浪费,存活对象较多的情况下复制对象的过程等会非常耗时,而且需要担保机制。
适合场景: 只有少量对象存活的场景,这也正是新生代对象的特点,所以一般新生代的垃圾回收器基本都会选择标记复制法。
3、标记整理法
特点: 相对于标记复制法不会浪费内存空间,相对标记清除法则可以避免空间碎片,但是速度比其他两个算法慢。
适合场景: 内存吃紧,又要避免空间碎片的场景,老年代想要避免空间碎片问题的话通常会使用标记整理法。
分代思想 & 分区思想
所谓分代思想,就是根据 JVM 内存的不同内存区域,采用不同的垃圾回收算法。例如对于存活对象少的新生代区域,比较适合采用复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。而对于老年代这种存活对象多的区域,比较适合采用标记压缩算法或标记清除算法,这样不需要移动太多的内存对象。
分代思想按照对象的生命周期长短将其分为了两个部分(新生代、老年代),但 JVM 中其实还有一个分区思想,即将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个区间,可以较好地控制 GC 时间。