【JVM原理】如何判定对象可以回收

前言

Github:jvm 博客代码

JVM内存结构

JVM类加载机制

JVM内存溢出分析

HotSpot对象创建、内存、访问

如何判定对象可以回收

垃圾收集算法

垃圾收集器

内存分配和回收策略

一 垃圾回收机制概述

GC(Garbage Collection)垃圾收集机制是指 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;

/**
 * JVM 参数配置:
 * -Xms20m -Xmx20m -XX:+PrintGCDetails -Xloggc:/Users/hongqi/gc.log
 * 
 * JVM 参数说明:
 * -Xms20m -Xmx20m 堆最大最小设置为 20M,限制堆内存大小为 20M
 * -XX:+PrintGCDetails 用于打印 GC 日志
 * -Xloggc:/Users/hongqi/gc.log 输出 GC 日志到指定目录
 *
 * @author yihonglei
 */
public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _MB = 1024 * 1024;
    /**
     * 这个成员属性的唯一意义就是占点内存,以便在 GC 日志中看清楚是否被回收过
     */
    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;

        // 告诉jvm,希望进行一次垃圾回收
        System.gc();
    }

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

GC详情:

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

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),

引用强度依次逐渐减弱。

1)强引用(Strong Reference)

强引用在程序里面使用最普遍,只要对象强引用还存在,表明对象还“活着”,GC 就不会回收掉

被引用的对象。比如:

Object strongReference = new Object()

当 Java 内存空间不足时,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,

也不会靠随意回收具有强引用的对象来解决内存不足的问题。 如果强引用对象不使用时,

需要释放引用,让 GC 能够回收 ,通过 StrongReference 类来实现软引用。 

2)软引用(Soft Reference)

软引用是用来描述一些还有用但是并非必需的对象。如果一个对象具有软引用,

当内存空间足够时,垃圾回收器就不会回收该对象,但是当内存空间不足时,

这些对象就会被回收掉。只要这些对象没有被 GC,则这些对象可以被程序使用。

软引用可以用来实现内存敏感的高速缓存,因为通过回收特性,不会影响 Java 内存,

是一个非常有自知之明的引用。通过 SoftReference 类来实现软引用。

3)弱引用(Weak Reference)

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

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

都会回收掉只被弱引用关联的对象。通过 WeakReference 类来实现弱引用。

4)虚引用(Phantom Reference)

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

引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,

在任何时候都可能被回收掉。为一个对象设置虚引用关联的唯一目的就是能在这个对象

被收集器回收时收到一个系统通知,用于追踪对象被 GC 的活动。

通过 PhantomReference 类来实现虚引用。

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

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

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

第一次标记:

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

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

覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视

为"没有必要执行"。如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会

被放置在一个叫做 F-Queue 的队列中,并在稍后由一个虚拟机自动建立的、低优先级的

Finalizer 线程去触发这个方法。

第二次标记:

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

最后一次机会:如果对象在其 finalize() 方法中重新与引用链上任何一个对象建立关联,

第二次标记时会将其移出"即将回收"的集合;如果对象没有,也可以认为对象已死,

可以回收了; 一个对象的 finalize() 方法只会被系统自动调用一次,经过 finalize() 方法逃脱

死亡的对象,第二次不会再调用;

参考文献

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

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值