JVM GC系列 — GC算法
一.前言
从本篇文章开始,将开始一个新的系列JVM。JVM是一个非常庞大且复制的技术体系,但是对于程序猿的升级,走向更高阶所必要经历的,曾经也下决心要好好学习一番,然而毅力不足都中途放弃。
GC的作用就是回收垃圾,但是要做到做点必须要解决两个问题:
- 如何确定哪些是垃圾
- 怎样回收垃圾
这两个问题可谓是GC的核心,本篇文章将从算法角度学习GC是怎样解决这两个问题。
### 二.如何确定哪些是垃圾
1.引用计数法
在Java应用中,可被回收的对象必然是无用对象,即没有其他对象引用它或者其脱离了应用的中的对象整体:
如上图,蓝色标记的对象之间相互引用,都是存活对象,GC不应该回收。而红色标记的对象,已经没有任何对象引用它,GC应该回收它。
从以上可以确定,GC首先应该回收没有任何引用指向其的对象。如何表达没有任何引用?
为每个对象维护一个计数器,该计数器表示指向其的引用。当没有引用其时,则计数器应该为0,GC则应该回收。这种算法被称为引用计数法。
如上图,当计数器为0时,表示该对象已经没有任何引用,可被垃圾收集器回收。该算法实现非常简单,且性能较佳,但是存在局限性。
2.引用计数法的局限性
引用计数法固然简单易于理解,但是它无法解决相互引用的问题。当两个以上的对象之间相互引用,但是它们已经脱离整个Java对象的整体时,引用计数法便无法表达出它们是可回收对象:
如上图,当两个红色的标记的对象相互引用,此外没有任何对象引用它们,显然它们是可回收对象。但是引用计数法会造成无法被回收。
假如有以下代码:
void referenceCount() {
...
A a = new A();
B b = new B(a);
a.setB(b);
...
}
A和B之间相互引用,但是此外没有任何对象引用A和B,那么当线程运行退出referenceCount方法时,A和B依然不能被回收。
3.可达性分析算法
由于引用计数法的局限性,显然它不适合作为JVM中定位可回收对象的算法。实际上再JVM中使用另一种方式表示对象是否可回收 — 可达性分析算法。
可达性分析算法:能从GC Roots找到一条到该对象的引用链路,则该对象就是存活对象,如何找不到,则该对象就是可回收对象。
这里首先要明白什么是GC Roots:
- 栈(栈帧中的本地变量表)中引用的对象
- 方法区中的静态成员
- 方法区中的常量引用的对象
- 本地方法栈中JNI(一般说的Native方法)引用的对象
Note:
简而言之,可达性分析算法就是根搜索算法,类似于树结构的搜索。其中GC Roots就是根,其他对象就是树上的节点。在根和节点之间寻找路径。
从图中可以看出,可达性分析算法能够解决相互引用的问题。从GC Roots开始到对象之间存在一条引用链路,则就是存活对象,反之即是死亡对象。
### 三.怎样回收垃圾
通过以上的可达性分析算法,GC能够找到堆区中的可回收对象,但是具体怎样回收才能具有更高的效率呢?在JVM中存在以下几种回收算法:
- 标记清除
- 复制
- 标记整理
以上三种算法都各有利弊,下面逐一介绍。
1.标记清除算法
顾名思义,该算法分为两个阶段,先标记出可回收对象,然后再清除这些对象。
将堆区看成以上连输的方块片段,红色代表没有GC Roots引用的对象,蓝色代表有引用。
标记清除算法,首先针对堆区对象进行标记,标记出没有GC Root是对象引用的对象。然后再将没有GC Roots引用的对象清除。
该算法的效率较高,只需要标记然后清除即可。但是从图中也可以看出问题所在,在清除后,会造成大量的不连续的内存碎片,当有大对象分配时,将又会触发GC,从而影响性能。
2.复制算法
为了解决标记清除算法的导致的内存碎片问题,复制算法因此而生。复制算法的思想是将内存分为两块,每次只使用其中一块,回收时将存活的对象复制到另一块中,然后将使用的那块完全清除,再使用新的复制那块。
经过复制算法后,不会再产生断断续续的内存碎片。每次垃圾回收之后,都能得到连续的大片内存。但是又产生了新的问题,内存是昂贵资源,将其分成两块,每次只使用其中一块,造成了资源浪费。但是实际中,对象基本上都是朝生夕死,能存活的对象少之又少,所以实际中一般不会按照1:1的比例分块。在Hostspot的年轻代默认按照8:1的进行划分,即Eden:Survivor=8:1。
3.标记整理算法
为了既解决标记清除算法带来的内存碎片问题,又能很好解决复制算法的内存牺牲问题。又出现标记整理算法。它类似标记清除,但是又增加了一个整理过程。即将存活对象往一端移动,整理内存碎片,形成连续内存。
通过标记清理,能够将回收垃圾对象,释放内存。通过整理压缩,能够形成连续内存,解决内存碎片。
虽然标记整理算法解决了复制算法和标记清除算法带来的问题,但是整理压缩也是耗费性能,降低效率。
4.分代算法
以上的GC回收算法都各有利弊,实际使用中,根据内存区域的划分以及其特点,HotSpot不单一采用以上的算法,而是根据堆区不同分代,分别采用以上算法,这种算法被称为分代算法。
由于Java对象具有朝生夕死的特点,堆区一般划分为新生代(年轻代)和老年代(年老代)。新生代对象生存周期短暂,老年代对象生成周期较长:
- 新生代对象存活周期短,每次只有少量对象存活,使用复制算法比较适宜。所以HotSpot中将新生代分为Eden和Survivor区域。
- 老年代对象存活时间长,每次GC后,大部分对象都存活。所以使用标记整理算法。
### 总结
本篇文章主要介绍GC中两个核心问题,第一:GC如何确定哪些对象是可回收的,在HotSpot中使用从GC Roots开始的可达性分析算法,定位对象是否存活。第二:在确定对象的存活与否后,采用何种策略回收对象。商业虚拟机中多数采用分代算法解决该问题。在了解了GC回收算法后,下篇文章再围绕这些算法学习HotSpot中的有哪些具体实现。