阿里架构师讲面试:最easy的JVM垃圾回收讲解

977 阅读6分钟

判断对象是否需要回收

  • 引用计数算法

循环引用场景会引起内存泄漏。

public class ReferenceCountingGC {
    public Object instance;

    public ReferenceCountingGC(String name){}
}

public static void testGC(){
    ReferenceCountingGC a = new ReferenceCountingGC("objA");
    ReferenceCountingGC b = new ReferenceCountingGC("objB");

    a.instance = b;
    b.instance = a;

    a = null;
    b = null;
}

如果先释放a/b.instance引用,再释放a/b引用,就不会出现内存泄露的情况。
这里先释放了a/b引用,就找不到a/b对象,也就没办法再释放a/b.instance引用。

  • 可达性分析法

可达性分析算法(Reachability Analysis)的基本思路是,通过根引用(GC Roots)作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。

Gc Roots:

  • 虚拟机栈****中的对象引用。<堆中的对象引用并没有作为GC Root>
  • 方法区中静态的****对象引用。
  • 方法区中的对象引用。
  • 本地方法栈中JNI(即一般说的 Native 方法)的对象引用。

参考:www.infoq.cn/article/ZOY…

回收算法

  • 标记清除算法<容易带来内存碎片>

  • 复制算法<空间利用率低>

内存空间划分为1区,2区。jvm标记可达对象,并复制到2区,1区清零。

  • 标记整理算法<效率低>

标记整理算法(Mark-Compact)标记过程仍然与标记 — 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。标记整理算法一方面在标记 - 清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。

  • 分代回收算法

  • jvm堆结构

  • 各区域回收算法

分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述 3 种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。jvm根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记 - 清理或者标记 — 整理算法来进行回收。

  • young区

  • 区域划分原理

为什么需要survivor

不就是新生代到老年代么,直接 Eden 到 Old 不好了吗,为啥要这么复杂。想想如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

为什么需要两个(包括from区和to区)

设置两个 Survivor 区最大的好处就是解决Survivor 区内存碎片化。

我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有 2 个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。

  • old区:

  • 回收机制

老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 — 整理算法

参考:

www.infoq.cn/article/ZOY…

mp.weixin.qq.com/s/aA1eDYIUH…

blog.csdn.net/aaa1117a8w5…

www.kancloud.cn/imnotdown10…

GC执行

  • GC类型

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式

  • Young GC:只收集young gen的GC。

  • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式。

  • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式。

  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

蚂蚁内部模式:yong gc+full gc,不存在Old GC和Mixed GC

参考:www.zhihu.com/question/41…

  • 对象进入old区条件

当对象首次创建时, 会放在新生代的eden区, 若没有GC的介入,会一直在eden区, GC后,是可能进入survivor区或者年老代:

  • **Survivor区满。**yong gc后survivor区剩余空间不足,无法承接eden区的存活对象。

  • 长期存活对象。当对象年龄达到一定的大小就会离开年轻代,进入老年代。 而对象的年龄是由GC的次数决定的-XX:MaxTenuringThreshold=n 新生代的对象最多经历n次GC,就能晋升到老年代。<默认为16跳>

  • 大对象**,减少from to区的复制成本****。除年龄外, 对象体积也会影响对象的晋升的, 若对象体积太大, 复制成本很高。**-XX:PretenureSizeThreshold 即对象的大小大于此值, 就会绕过新生代, 直接在老年代分配。

  • Full gc触发条件

  • 当准备要触发一次 young GC时,如果发现统计数据说之前 young GC的平均晋升大小比目前的old gen剩余的空间大,则不会触发young GC而是转为触发 full GC。

  • 大对象直接到old区申请空间,发现old区内存不够。

觉得有收获的话帮忙点个赞吧,让有用的知识分享给更多的人

## 欢迎关注掘金号:五点半社

## 关注微信公众号:五点半社(工薪族的财商启蒙)##