目录

  • 一、判断对象是否可以回收垃圾回收常见算法
  • 1. 引用计数法
  • 2. 可达性分析算法
  • 二、垃圾收集算法
  • 1. 标记-清除法
  • 2. 标记压缩算法
  • 3. 复制算法
  • 4. 分代算法


一、判断对象是否可以回收垃圾回收常见算法

自动化的管理内存资源,垃圾回收机制必须要有一套算法来进行计算,哪些是有效的对象,哪些是无效的对象,对于无效的对象就要进行回收处理。
常见的垃圾回收算法有:引用计数法、标记清除法、标记压缩法、复制算法、分代算法等。

1. 引用计数法

引用计数是历史最悠久的一种算法,最早George E. Collins在1960的时候首次提出,50年后的今天,该算法依然被很多编程语言使用

原理

  • 如果一个对象被引用了,则它的引用计数加一
  • 当引用计数为0时,则可以作为垃圾回收

优点:

  • 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
  • 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember错误。
  • 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象

缺点:

  • 每次对象被引用时,都需要去更新计数器,有一点时间开销。
  • 浪费CPU资源,即使内存够用,仍然在运行时,进行计数器的统计。
  • 无法解决循环引用问题。(最大的缺点)
    什么是循环引用?

弊端:循环引用

如下图所示:

静态HTML docker_静态HTML docker

  • 这两个对象的引用计数都为1
  • 但是这两个对象都没有在使用,也无法进行垃圾回收,造成内存泄漏

如下面代码所示

class TestA {
    public TestB b;
}

class TestB {
    public TestA a;
}

public class Main {
    public static void main(String[] args) {
        TestA a = new TestA();
        TestB b = new TestB();
        a.b = b;
        b.a = a;
        a = null;
        b = null;
    }
}

虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收

如下图所示

静态HTML docker_内存空间_02

2. 可达性分析算法

  • 从GCroot对象作为起始点,向下搜索,搜索的路径称为引用链
  • 如果一个对象到GCroot中没有任何一条引用链,则认为该对象死亡

可以作为GCroot的对象

  • 虚拟机栈中的本地变量表中引用的对象
  • 方法区类静态属性引用的对象
  • 方法区常量引用的对象
  • 本地方法栈中本地方法引用的对象

引用

参考:

回收方法区

  • 方法区的对象回收效率很低,主要回收废弃常量和无用的类
  • 无用的类有:Java堆没有该类的实例;加载该类的ClassLoader已经被回收;没有被反射
  • 注:在大量使用反射、动态代理等场景时,需要虚拟机具备类卸载的功能,防止方法区溢出。

二、垃圾收集算法

1. 标记-清除法

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。

  • 标记:从根节点开始标记引用的对象。
  • 清除:未被标记引用的对象就是垃圾对象,可以被清理。

原理

  • 确定一系列根对象,指那些肯定不能被当做垃圾回收的对象
  • 在进行垃圾回收时,会对所有对象进行扫描,判断这些对象是否被根对象直接或间接的引用,如果是,则这个对象不能回收,反之可以被回收
  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收

参考下图:

静态HTML docker_内存空间_03

这张图代表的是程序运行期间所有对象的状态,它们的标志位全部是0(也就是未标记,以下默认0就是未标记,1为已标记),假设这会儿有效内存空间耗尽了,JVM将会停止应用程序的运行并开启GC线程,然后开始进行标记工作,按照根搜索算法,标记完以后,对象的状态如下图

静态HTML docker_内存空间_04


可以看到,按照根搜索算法,所有从root对象可达的对象就被标记为了存活的对象,此时已经完成了第一阶段标记。接下来,就要执行第二阶段清除了,那么清除完以后,剩下的对象以及对象的状态如下图所示

静态HTML docker_引用计数_05


可以看到,没有被标记的对象将会回收清除掉,而被标记的对象将会留下,并且会将标记位重新归0。接下来就不用说了,唤醒停止的程序线程,让程序继续运行即可。

优点:

  • 可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。

缺点:

  • 效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的
  • 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的

如下图所示:

标记

静态HTML docker_静态HTML docker_06

清除


静态HTML docker_引用计数_07


可以看出

碎片化很严重

,这是其最大的缺点

为什么不能边运行,边标记?

  • 因为标记和清理的过程是遍历全部对象
  • 如果代码没有停止运行,就无法梳理所有的对象关系,此时的标记和清除是不准的,所以必须停止应用程序

2. 标记压缩算法

标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题

原理

静态HTML docker_jvm_08

优点:
同标记清除算法,解决了标记清除算法的碎片化的问题缺点:
标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响

3. 复制算法

  • 复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收
  • 如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。

如下图所示:

静态HTML docker_内存空间_09

标记

静态HTML docker_静态HTML docker_10


复制


静态HTML docker_静态HTML docker_11


清除

静态HTML docker_内存空间_12


交换角色


静态HTML docker_引用计数_13

JVM中年轻代内存空间

静态HTML docker_引用计数_14

  1. 在Minor GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。(默认Eden为80%,From为10%)
  2. 紧接着进行Minor GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。(默认15
  3. 经过这次Minor GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
  4. Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

优点:

  • 在垃圾对象多的情况下,效率较高
  • 清理后,内存无碎片

缺点:

  • 垃圾对象少的情况下,不适用,如:老年代内存
  • 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低

4. 分代算法

  • 前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的选择。
  • 分代算法其实就是这样的,根据回收对象的特点进行选择,在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法

静态HTML docker_内存空间_15

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW( stop the world)的时间更长

哪些对象可以作为GC Root?

可以利用eclipse提供的Memory Analyzer(MAT).

/**
 * 演示GC Roots
 */
public class Demo2_2 {

    public static void main(String[] args) throws InterruptedException, IOException {
        List<Object> list1 = new ArrayList<>();
        list1.add("a");
        list1.add("b");
        System.out.println(1);
        System.in.read();

        list1 = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end...");
    }
}

利用 jmap -dump:format=b, live,file=1.bin xxxx抓取堆内存的快照

  • format:文件类型,b代表2进制文件
  • live:代表只抓取没有被垃圾回收的对象
  • file:存储的路径
  • xxxx:进程id

分为四类:

  • System Class:系统加载类,是核心的类对象
  • Native Stack : 操作系统调用时引用的java对象
  • Thread :活动的线程,线程中栈帧内的对象,局部变量或方法参数引用的对象
  • Busy Monitor :加锁的对象