JVM垃圾回收机制(1)--如何判定对象可以回收

前言

Github:https://github.com/yihonglei/jdk-source-code-reading(java-jvm)

JVM内存结构

JVM类加载机制

JVM内存溢出分析

HotSpot对象创建、内存、访问

JVM垃圾回收机制(1)--如何判定对象可以回收

JVM垃圾回收机制(2)--垃圾收集算法

JVM垃圾回收机制(3)--垃圾收集器

JVM垃圾回收机制(4)--内存分配和回收策略

一 垃圾回收机制概述

    GC即垃圾收集机制是指JVM用于释放那些不再使用的对象所占用的内存。Java语言并不要求JVM有GC,

也没有规定GC如何工作。不过常用的jvm都有GC,而且大多数GC都使用类似的算法管理内存和执行收集操作。

在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能。有些垃圾收集专用于特殊的应用程序。

比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率。

理解了应用程序的工作负荷和JVM支持的垃圾收集算法,便可以进行优化配置垃圾收集器。

垃圾收集的目的在于清除不再使用的对象。

GC主要考虑三件要完成的事情:

1)哪些内存需要回收?(对象是否可以被回收的两种经典算法: 引用计数算法和可达性分析算法

2)什么时候回收?(堆的新生代、老年代、永久代的垃圾回收时机,MinorGC 和 FullGC)

3)如何回收?(经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法和七种垃圾收集器)

这里主要讨论哪些内存需要回收。

二 如何确定对象是否存活

    垃圾收集器对堆进行回收前,首先需要确定哪些对象还"活着",哪些对象已经"死去"。

常用两种算法分析对象是否存活: 引用计数算法和可达性分析算法。

1、引用计数算法

算法思路:

1)给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;

2)当引用失效时,计数器值就减1;

3)任何时刻计数器为0的对象就是不可能再被使用的;

算法优点:

实现简单,判定效率高;

算法缺点:

    很难解决对象之间相互循环引用的问题。所以主流的虚拟机里没有选用引用计数算法来管理内存的。

关于循环引用的例子,比如对象objA和objB都有字段instance,赋值令objA.instance = objB,

objB.instance = objA,除此之外对象再无任何引用,实际上两个对象已经不可能再被访问,

但是他们因为相互引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

循环引用代码:

package com.jpeony.jvm.gc;

/**
 * VM Args: -XX:+PrintGCDetails
 * 参数描述: 用于打印GC日志,可以指定GC文件目录-XX:+PrintGCDetails -Xloggc:d:\gc.log
 * 
 * @author yihonglei
 */
public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _MB = 1024 * 1024;
    private byte[] bigSize = new byte[2 * _MB];

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance  = objA;

        objA = null;
        objB = null;

        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}

GC详情:

JVM垃圾回收机制(1)--如何判定对象可以回收_第1张图片

从运行结果可以看到内存回收了,说明虚拟机不是用的引用计数算法管理内存。

2、可达性分析算法

算法思路:

    通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始往下搜索,搜索所有走过的路径

称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

如图:

可达性分析算法判定对象是否可以回收的图例中,绿色部分为仍然存活的对象,黄色部分判定可回收的对象。

黄色部分object5、object6、object7虽然相互关联,但是它们到GC Roots是不可达的,也就是没有与GC Roots

建立引用链,所以它们将被判定为是可以回收的对象。

在Java语言中,可以作为GC Roots的对象包括下面几种:

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

2)方法区中静态属性引用的对象。

3)方法区中常量引用的对象,在jdk7后被移到堆中。

4)本地方法栈中JNI(即一般说的Native方法)引用的对象。

算法优点:

更加精确和严谨,可以分析出循环数据结构相互引用的情况;

算法缺点:

1)实现比较复杂;

2)需要分析大量数据,消耗大量时间;

3)分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",

是垃圾回收重点关注的问题);

三、再谈引用

在JDK1.2以前的引用定义:

    在JDK1.2以前,Java中的引用定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存

的起始地址,就称这块内存代表着一个引用。这种定义太过狭隘,一个对象在这种定义下只有被引用或者没有

被引用两种状态,一点犹豫的机会都没有,对于哪种"食之无味,弃之可惜"的鸡肋对象就没法描述了。但是,

我们又希望能描述这类对象,当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是

非常紧张,则可以抛弃这些对象。

在JDK1.2之后对引用做了补充:

    在JDK1.2之后,Java对象引用概念进行了扩充,将引用分为强引用(Strong Reference)、

软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4中,

引用强度依次逐渐减弱。

1)强引用(Strong Reference)

强引用在程序里面使用最普遍,只要对象强引用还存在,GC就不会回收掉被引用的对象。比如:

Object strongReference = new Object()

当Java内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象

来解决内存不足的问题。 如果强引用对象不使用时,需要释放引用,让GC能够回收 ,比如:

strongReference = null;

    通过StrongReference类来实现软引用。 

2)软引用(Soft Reference)

    软引用是用来描述一些还有用但是并非必需的对象。如果一个对象具有软引用,当内存空间足够时,垃圾回收器就不会

回收该对象,但是当内存空间不足时,这些对象就会被回收掉。只要这些对象没有被GC,则这些对象可以被程序使用。

    软引用可以用来实现内存敏感的高速缓存,因为通过回收特性,不会影响Java内存,是一个非常有自知之明的引用。

    通过SoftReference类来实现软引用。

3)弱引用(Weak Reference)

    弱引用也是用来描述非必需的对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象

只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收

掉只被弱引用关联的对象。

    通过WeakReference类来实现弱引用。

4)虚引用(Phantom Reference)

    虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,

完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,在任何时候都可能被回收掉。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,用于追踪

对象被GC的活动。

    通过PhantomReference类来实现虚引用。

对象引用关系参考:https://juejin.im/post/5b82c02df265da436152f5ad

四 判断对象是生存还是死亡

    即使在可达性分析算法中不可达的对象,也并非是"非死不可的",只是暂时处于"缓刑"阶段,

要真正宣告一个对象死亡,至少要经历两次标记过程。

第一次标记:

    如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记

并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,

或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行"。

    如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的

队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去触发这个方法。

第二次标记:

    GC将对F-Queue队列中的对象进行第二次小规模标记;finalize()方法是对象逃脱死亡的最后一次机会:

如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;

如果对象没有,也可以认为对象已死,可以回收了;                    

一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

参考文献

《深入理解Java虚拟机》 (第二版) 周志明 著;

你可能感兴趣的:(#,---JVM,Thinking,In,JVM)