前边讲到 JVM 运行时内存的地方,关于新生代、老年代中 GC 垃圾回收以及垃圾回收算法,不知是否有点懵懵的,这篇一起了解一下垃圾回收以及垃圾回收算法。
一、垃圾回收需要完成的三件事
- 哪些内存需要回收?——垃圾对象(如何判定对象为垃圾对象)
- 如何回收?——垃圾回收算法
- 何时回收?
1. 哪些内存需要回收?
判定为 "死" 对象,或者无用对象时即视为可回收内存。
如何判定为垃圾对象,在这有两个方法。
1.1 引用计数算法
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。
简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
这种方法的效率非常的高,但是却有一个很大的缺点,无法解决相互引用的对象,导致进入计数的死循环,致使引用计数算法无法通知 GC 收集器回收他们。
因为这种弊端,在主流的 Java 虚拟机里面没有选用计数算法来管理内存的。
1.2 可达性分析算法
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
2. 如何回收?——垃圾回收算法
2.1 标记-清除算法(几乎都不用)
该算法为最基础的垃圾收集算法,如同名称一样,该算法分为 "标记" 和 "清除" 两个阶段。
标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图
从图中我们就可以发现,该算法最大的问题是内存碎片化严重,空间碎片太多,内存不足时,很容易触发下一次 GC。
2.2 复制算法(新生代使用)
为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块用完了,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次性清理掉,每次都对整个半区进行内存回收,不需要考虑碎片问题,如图:
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
补充:现在商业虚拟机都是采用复制算法来回收新生代,对象在创建时,虚拟机将内存划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和一块 Survivor , 当回收时,将 eden 和 Survivor 中还存活着的对象一次性的复制到另一块 Survivor 空间上,然后清理掉 eden 和刚才用过的 Survivor 空间。
hotspot 默认 Eden 和 Survivor 比例为:8:1,也就是只有 10% 的空间被浪费。
2.3、标记整理算法(老年代使用)
标记整理,跟 标记-清除 算法中的标记过程是一样的,只是后续不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端界边界以外的内存。如图:
2.4、分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。
老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
3. 何时回收?
何时回收之前,先了解一下几种 GC。
- Minor GC:该 GC 会清理年轻代的内存,GC 时会触发 "全世界的暂停"
- Major GC/Full GC:清理老年代。
如下是几种 GC 的触发场景:
- 执行 system.gc() 的时候
- 老年代空间不足,GC 之后,空间不足会触发 outofmemoryError
- 永久代空间不足,GC 之后,空间不足会触发 java.outofMemory PerGen Space
- Minor GC之后 Survior放不下,放入老年代,老年代也放不下,触发FullGC,或者新生代有对象放入老年代,老年代放不下,触发FullGC
- 新生代晋升为老年代时候,老年代剩余空间低于新生代晋升为老年代的速率,会触发老年代回收
- new 一个大对象,新生代放不下,直接到老年代,空间不够,触发FullGC
二、重新疏导 JVM GC 过程。
Java 堆是垃圾收集器管理的主要区域,该区域又分为新生代、老年代,而新生代又分为一个 Eden 区和两个 Survivor 区,关于对象的创建,优先在 Eden 区中分配,当 Eden 区没有足够空间时,虚拟机将触发一次 Minor GC,由于大多数对象都是朝生夕灭,所以 Minor GC 非常频繁,速度也非常快。
Full GC,发生在老年代的 GC,当老年代没有足够的空间时即发生 Full GC,发生 Full GC 一般都会有一次 Minor GC,我们在这将 Full GC 等价于 Major GC,也许会有疑问,Full GC 跟 Major GC 到底有什么区别呀,由于许多 Major GC 是由 Full GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的,在这就不继续填坑了,感兴趣的小伙伴可以自行搜索了解。
三、最后总结
首先要清楚垃圾回收要干的三件事,哪些需要回收、如何回收以及何时回收。
通过 "引用计数算法"(不推荐) 或 "可达性分析算法" 来判定需要回收的对象,知道了需要哪些是需要回收的对象,再通过垃圾回收算法(复制、标记清除、标记整理、分代收集)实现垃圾回收。
补一张图:
如果文章有错的地方欢迎指正,大家互相留言交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:niceyoo