Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。然后再进行回收。
重点
如何判断是否存活(如何判断是不是垃圾)?
怎么回收垃圾?
死亡对象的判断算法
引用计数描述的算法为
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采
用引用计数法进行内存管理。但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题。如下图所示
类似这种互相引用的场景就会导致内存泄漏
可达性分析算法
此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。以下图为例:
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。
在Java语言中,可作为GC Roots的对象包含下面几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
2. 方法区中类静态属性引用的对象;
3. 方法区中常量引用的对象;
4. 本地方法栈中 JNI(Native方法)引用的对象。
也就是在jvm内存模型中没有其他地方指向到堆的对象,如下图所示
垃圾回收算法
标记-清除算法
"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,后续的收集算法都是基于这种思路并对其不足加以改进而已。
不足:
1. 效率问题 : 标记和清除这两个过程的效率都不高
2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。(碎片严重,内存不连贯)
复制算法
复制算法的基本思想是将可用内存分为两个半区,任一时刻只有一个半区被使用。当一个半区的内存即将用完时,垃圾回收过程开始将活动对象复制到另一个半区,然后清除原半区的所有对象。
有点是:
1.减少内存碎片化
- 由于每次复制过程都只将活动的对象复制到另一半区,并且这些对象是连续放置的,因此复制算法极大地减少了内存碎片的问题。这样可以确保应用程序在需要分配大块内存时更加高效,避免了因内存碎片化导致的性能下降
2. 提高垃圾回收效率:
- 复制算法通常只处理存活的对象,而不是遍历所有对象。这意味着如果存活对象的数量远小于总对象的数量,那么复制算法的效率将会非常高。此外,由于内存是分块管理的,回收一块内存只需简单地将其空闲,而无需逐个对象地清理。
缺点:
1. 在任一时刻,只有一半的内存是可用的,这可能导致内存资源的浪费
2.当存活对象的数量较多或对象较大时频繁地复制对象也可能对性能造成影响
标记-整理算法
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。