Java运行时内存中的程序计数器、虚拟机栈、本地方法栈这三部分区域其生命周期与相关线程有关,随线程而生,随线程而灭。而程序计数器就是一个单纯存地址的整数也不需要关心,因此我们GC(垃圾回收)的主要目标就是堆(堆中存放着几乎所有实例对象)!
检测垃圾
一个对象,如果后续不再被使用且没有引用指向它,就可以认为是垃圾。
有以下方法可知对象是否有引用指向:
引用计数算法
在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题 Python,PHP采取了引用计数算法。
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即可被回收。
Test test1 = new Test(); Test test2 = test1; Test test3 = test1; Test test4 = test1;
此算法存在两个缺陷:
- 浪费内存空间
- 存在循环引用的情况
用一个例子来解释一下循环引用的问题:
class Test { public Test test; } Test test1 = new Test(); Test test2 = new Test(); test1.test=test2; test2.test=test1;
此时test1与test2 销毁了,两个对象的引用计数分别减一。
此时这两个对象的引用计数不为0,不能作为垃圾且无法使用,陷入了一个逻辑上的循环。
可达性分析算法
通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。
在java中,可作为GC Roots(垃圾回收根对象)的对象有以下几种:
- 栈上的局部变量
- 常量池中引用的对象
- 方法区中类静态属性引用的对象
只有从GC Roots对象开始,通过引用链可达的对象才被认为是存活的,而无法通过引用链访问的对象则会被判定为垃圾,并进行回收。
缺点:
- 遍历开销:可达性分析算法需要遍历整个对象图以确定每个对象的可达性。对于大型堆和复杂的引用关系,遍历开销可能非常大,特别是在全局垃圾回收中。这可能会导致垃圾回收的性能下降。
- 延迟回收:可达性分析算法需要遍历整个对象图,从GC Roots开始,逐个检查每个对象的引用关系。这个过程可能需要消耗大量的时间,且在垃圾回收期间,应用程序的执行会被暂停(STW - Stop-The-World)。因此,可达性分析算法可能导致较高的延迟,影响程序的响应性能。
回收垃圾
标记清除算法
算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
回收前:
回收后:
不足:
- 标记和清除这两个过程的效率都不高。
- 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
复制算法
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
简单来说复制算法就是把不是垃圾的对象拷贝到未使用的那一边,然后再统一释放刚才使用过的那一块区域。
缺点:
- 内存利用率比较低。
- 如果当前区域的大部分对象都需要保留垃圾很少,那么此时的复制成本就比较高。
标记整理算法
标记整理算法的基本流程:
- 标记:从根对象开始,通过可达性分析算法,标记所有从根对象可达的存活对象。标记过程中,通常使用标记位(标记状态)来标记对象是否为存活对象。
- 整理:将所有存活对象移到堆的一端,紧凑排列,以便释放出连续的一段内存空间。在移动存活对象时,需要更新对这些对象的引用,确保引用指向移动后的新位置。
- 更新引用:在堆中,遍历所有存活对象的引用,将其指向新的位置。这是为了避免悬挂指针(引用指向被移动或已回收的对象)的问题。
- 释放未标记的对象:在整理后的堆中,所有没有被标记的对象都可以被认为是垃圾对象,可以直接被回收。
优点:解决了内存碎片问题。
缺点:搬运复制开销比较大。
分代算法
分代算法主要基于一种观察:大部分对象的生命周期都比较短暂。根据这个观察,分代算法将堆内存划分为不同的代(Generation),每一代中对象的生命周期不同,并且根据对象的生命周期将不同的垃圾回收策略应用于不同的代中。
一般来说,分代堆内存被划分为年轻代(Young Generation)和老年代(Old Generation)两个主要部分
年轻代:
年轻代是存放新创建的对象的地方,大部分对象在创建后很快就变为垃圾对象。年轻代通常进一步分为Eden区和两个Survivor区。新创建的对象首先放入Eden区,当Eden区满时,不会被回收的对象会被转移到一个Survivor区。当一个Survivor区满时,其中的存活对象会被复制到另一个Survivor区或者老年代。经过多次垃圾回收后依然存活的对象将晋升到老年代。
年轻代通常采用复制算法(Copying)作为垃圾回收策略,因为新创建的对象的生命周期短暂,复制算法可以更好地利用对象的特点。
老年代:
老年代是存放存活时间较长的对象的地方,老年代的对象生命周期较长,垃圾回收频率相对较低。对于老年代的垃圾回收,可以采用标记-清除(Mark-Sweep)或者标记-整理(Mark-Compact)等算法。
分代算法通过区分不同代的对象,针对不同代采取不同的垃圾回收策略,可以提高垃圾回收效率和系统性能。年轻代的频繁垃圾回收可以快速回收新创建对象,老年代的较少回收可以减少全局垃圾回收的引发,提高应用程序的响应性。这种分代垃圾回收策略在大多数的垃圾收集器中都有应用。