JVM垃圾回收

看到垃圾回收,首先你会想到什么?

1、什么是垃圾?

2、哪些地方的垃圾需要被回收?

3、如何定位垃圾?

4、如何回收垃圾?

5、什么时候回收垃圾?

下面,我们将带着这5个问题来进行分析。

1、什么是垃圾?

JVM中的垃圾指的是无用的内存,这些内存中的数据若在后续处理过程中不再被使用,那么我们将是为垃圾进行回收,以保证有足够的内存可用。

程序在运行过程中会存在两个问题:

1)内存溢出:是指内存空间分配不足

2)内存泄露:是指内存使用完后无法被回收

2、哪些地方的垃圾需要被回收?

线程私有内存区域(程序计数器、虚拟机栈、本地方法栈)会随着线程的结束而释放,栈中的栈帧随着方法的进入和退出有条不紊的执行者进栈和出栈操作,每个栈帧中的分配多少内存在编译期便已确定,因此无需对该区域进行垃圾回收。

线程共享内存区域(方法区和Java堆)需要分配的内存大小只有在运行期才知道,因此这部分的内存是动态分配和回收的,也是我们接下来需要对垃圾内存回收的区域。

3、如何定位垃圾?

因为我们垃圾回收的主要区域是针对于Java堆(又称为GC堆),这部分区域主要存放的是Java对象的实例,因此判断内存是否可以被回收只需要确认实例对象已经不被使用,即对象是否存活。

判断对象是否的方法有两种,一种是引用计数算法,另一种是可达性分析算法。下面我们来介绍一下这两中算法的优缺点。

1)引用计数算法

首先我们来说一下这个算法的实现思想:为每个对象添加一个引用计数器,每当有一个地方引用它时,计数器的值加1,当引用失效时,计数器的值减1。当计数器的值为0时表示该对象没有被引用。

上述实现思想看起了一点毛病都没有,实现起来也是非常的简单,而且效率也比较高,但是既然会有其他的算法被使用,那么该算法也就一定有他的缺点。

优点:实现简单且效率高

缺点:无法处理对象互相循环引用的情况。

对象的互相循环引用?听起来不太容易理解,那我们就来举个例子吧

public class ReferenceCountingGC {

    public Object instance = null;

    public static void testGC () {

        ReferenceCountingGC objA = new ReferenceCountingGC();

        ReferenceCountingGC objB = new ReferenceCountingGC();

        objA.instance = objB;

        objB.instance = objA;

        objA = null;

        objB = null;

        System.gc();

    }

}

上述代码中,对象A和对象B彼此引用,虽然对其句柄赋值为null,但是两个对象的引用计数器的值均为1,因此使用引用计数算法无法对上述两个对象进行回收。

2)可达性分析算法

目前主流的程序语言的实现中都称是通过可达性分析算法来判断对象是否存活。其算法的基本思想是:通过一系列被称为”GC Roots“的对象作为起始点,开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何的引用链时,则证明此对象是不可用的。

上面提到了一个“GC Roots”,那么哪些对方的对象是可以作为GC Roots呢?

在Java语言中,可作为GC Roots的对象包括:

  a. 虚拟机栈(栈帧中的本地变量表)中引用的对象

  b. 本地方法栈中JNI(native方法)引用的对象

  c. 方法区中类静态属性引用的对象

  d. 方法区中常量引用的对象

4、如何回收垃圾?

关于如何回收垃圾这一部分我们要分为两部分进行讲解:垃圾收集算法垃圾收集器

第一部分 垃圾收集算法

垃圾收集算法主要包括三种算法:标记-清除算法复制算法标记-整理算法

另外还有分代收集算法,这种算法没有特别的思想,而是根据对象存活周期的不同将内存划分为几块,然后根据对应内存区域的特点采用适当的收集算法。

下面我们分别对上述三种算法进行分析说明。

1)标记-清除算法

实现思路:分为两个阶段,第一个阶段是标记,如何标记(定位)垃圾已经在第3点中介绍过了。第二个阶段是直接清除。

标记-清除算法的执行过程如图所示。它存在两个不足之处:

      a. 效率问题,标记和清除l两个过程的效率都不高。

      b. 空间问题,标记清除之后会产生大量不连续的内存碎片,可能会导致在后续分配较大对象是没有足够的空间导致出发一次内存收集的动作。

图片引用于https://www.jianshu.com/p/5d612f36eb0b
图片引用于https://www.jianshu.com/p/5d612f36eb0b

2)复制算法

实现思路:将内存按照大小划分为两块,每次只使用其中的一块,当另一块的内存用完了,就将还存活的对象复制到另一块的上面,然后再把当前内存区域一次清理掉。

优点:内存分配时只需要按照顺序分配即可,实现简单,运行效率高。

缺点:牺牲了一半的内存空间

目前商业虚拟机并非将内存空间按照1:1的比例来划分内存,而是将内存分为一块较大空间的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。回收时将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,每次新生代中可用的内存空间为整个新生代容量的90%。这里面临着两个问题:一是什么场景适合使用该方式,二是如果Eden和Survivor存活的对象超过10%该怎么处理?

        适合使用的场景:在绝大数情况下,Eden和Survivor在回收后存活的对象小于10%

        超过10%的处理方式:内存分配担保。我们将内存空间分为新生代(Eden和Survivor)和老年代,如果Eden和Survivor回收后的存活对象超过10%,则直接进入老年代。

图片引用于https://www.jianshu.com/p/5d612f36eb0b
图片引用于https://www.jianshu.com/p/5d612f36eb0b

3)标记-整理算法

实现思路:类似于标记-清除算法,不同的是不直接对可回收的对象进行清理,而是让所有存活的对象都想前一端移动,然后清理掉端边界以外的内存。

优点:不会产生大量不连续的内存空间,适用于老年代

图片引用于https://www.jianshu.com/p/5d612f36eb0b
图片引用于https://www.jianshu.com/p/5d612f36eb0b

4)分代收集算法

将Java堆划分为新生代和老年代。

新生代在每次垃圾收集时会有大批对象死去,适合采用复制算法。

老年代对象存活率高且没有额外空间进行分配担保,因此适合采用标记-整理算法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • GC区域Eden Survivor(from,to), Old Gen和Perm Gen VM区域总体分两类,he...
    Fitz_Lee阅读 446评论 0 0
  • 0. 前言 JVM笔记系列,以JDK1.7为基准,主要以《深入理解Java虚拟机》(第二版)和《Java虚拟机规范...
    郭寻抚阅读 947评论 0 3
  • JVM垃圾回收 Java堆中存放着大量的Java对象实例,在垃圾收集器回收内存前,第一件事情就是确定哪些对象是“活...
    azmohan阅读 361评论 0 0
  • 胶质瘤的分类和病理诊断 “胶质瘤”这个术语指的是具有与“正常胶质细胞”(即星形胶质细胞、少突胶质细胞和室管膜细胞)...
    咏而归怒而飞阅读 2,039评论 0 0
  • 2017.8.15 六月廿四 星期二 晴 我觉得我在乎的东西,已经越来越少了。心越大,心越累,不如放下。 但总有一...
    故园小屋阅读 202评论 0 0