JVM高级特性与实践(二):对象存活判定算法(引用) 与 回收

关于垃圾回收器GC(Garbage Collection),多数人意味它是Java语言的伴生产物。事实上,GC的历史远比Java悠远,于1960年诞生在MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp尚在胚胎时期,开发人员就在思考GC需要完成的3件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

目前GC早已解决了以上问题,内存的动态分配与内存回收机制已经相当成熟,一切似乎“自动化”起来。而开发人员仍旧需要了解GC和内存分配等底层知识,因为在排查各种内存溢出、内存泄漏问题、垃圾收集成为系统达到更高并发量的瓶颈时,开发人员需要对这些“自动化”技术实施必要的监控和调节。

在上一篇博文中介绍了Java内存运行时区域的各个部分,其中

  • 程序计数器虚拟机栈本地方法栈 3个区域随着线程而生,也随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配的内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内不需要过多考虑回收问题,因为方法结束或线程结束时,内存自然跟随着回收了。
  • Java堆方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器关注的是这部分内存,后续讨论的“内存”分配回收也是指这一块,尤其需要注意。

JVM高级特性与实践(一):Java内存区域与内存溢出异常


一. 对象是否已死

在堆里存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,首要的就是确定这些对象中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。

1. 引用计数算法(Reference Counting)

(1)算法含义

很多教科书判断对象是否存活的算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。

(2)算法效率分析

我相信大部分人对这个算法并不陌生,客观地说,引用计数算法的实现简单,判定效率也很高,大部分情况下是一个不错的算法。例如微软公司的COM(Component Object Model)技术、使用ActionScript3的FlashPlayer、Python语言都是用了该算法进行内存管理。但是Java虚拟机中并没有引用该算法来管理内存,最主要的原因是它很难解决对象之间互相循环引用的问题。

(3)举例证明

举个例子来证明,以下代码中的 testGC() 方法:对象objA 和对象objB都有字段instance,赋值令objA.instance = objB; 、objB.instance = objA;,除此之外,这两个对象再无引用,实际上这两个对象不可能再被访问,但是他们互相引用着对方,导致它们引用计数不为0,所以引用计数器无法通知GC收集器回收它们。

【引用计数器的缺陷】

/**
 * testGC()方法执行后,objA和objB会不会被GC呢?
 */
public class ReferenceCountingGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    /**
     * 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
     */
    private byte[] bigSize = new byte[2 * _1MB];

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

        objA = null;
        objB = null;

        // 假设在这行发生GC,objA和objB是否能被回收?
        System.gc();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

运行结果:

[Full GC (System)  [Tenured: 0k->210K(10240K),  0.0149142 secs] 4603K->210K(19456K),
[Perm : 2999K->2999K(21248K)], 0.0150007 secs]  [Times: user=0.01 sys=0.00, real=0.02 secs]
  • 1
  • 2
  • 1
  • 2

结果分析:

从运行结果可发现,GC日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。



2 . 可达性分析算法(Reachability Analysis)

(1)算法含义

在主流的商用程序语言(JavaC#、甚至是最古老的Lisp)的实现中,都是通过可达性分析来判定对象是否存活的。此算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots 到这个对象不可达)时,则证明此对象时不可用的。

(2)图解说明

如下图举例所示,对象object5、object6、object7虽然互相有关联,但是它们到GCRoots是不可达的,所以它们将会被判定为可回收对象。

(3)Java中可作为GCRoots的对象

在Java中,可作为GCRoots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象


3. 再谈引用(强、软、弱、虚引用)

(1)“引用”旧概念

无论时通过计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK1.2以前,Java中的引用定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

这种定义很纯粹但过于狭隘,一个对象中在这种定义下只有被引用或者没有被引用两种状态,缺少另外一类对象的描述:当内存空间足够时,能保留在内存中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

(2)“引用”新概念

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,引用强度依次逐渐减弱。4种引用解释如下:

  • 强引用:就是指在程序中普遍存在的,类似Obejct obj = new Object() 这种引用。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:用来描述一些还有用但并非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用:也是用来描述非必需对象,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
  • 虚引用:也称为幽灵引用或幻影引用(好炫的称号hhh),它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。


4 . 生存还是死亡

to be or not to be, this is a question.

(1)宣判对象“死亡”的过程

扯远了,回到正文来。即使在可达性分析算法中不可达的对象,也并非是要宣判“死亡”的,它们暂时都处于“缓刑”阶段,要真正宣告一个对象“死亡”,至少要经历两次标记过程:

  • 第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finaliza() 方法。
  • 第二次标记:当对象没有覆盖finaliza() 方法,或者finaliza() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

(2)finaliza()方法 —– 对象逃脱“死亡”的最后机会

如果这个对象被判定为有必要执行finaliza() 方法,那么此对象将会放置在一个叫做 F-Queue 的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发此方法,但并不承诺会等待它运行结束,原因是:如果一个对象在finaliza() 方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能导致F-Queue 队列中的其它对象永久处于等待,甚至导致整个内存回收系统崩溃。

finaliza() 方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue 队列中的对象进行第二次小规模的标记。如果对象想在finaliza() 方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,例如把自己(this关键字)赋值给某个类变量或者对象的成员变量,这样在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,基本上它就被回收了。

(3)实例证明对象的自救

通过以下代码展示一个对象的finaliza()被执行,但是它仍然可以存活的例子:

【一次对象自我拯救的演示】
/**
 * 此代码演示了两点:
 * 1.对象可以在被GC时自我拯救。
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 * @author zzm
 */
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }

        // 下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

运行结果:

finalize method executed!
yes, i am still alive :)
no, i am dead :(
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

结果分析:

由以上结果可知,SAVE_HOOK 对象的finalize() 方法确实被GC收集器触发过,并且在收集前成功逃脱了。

另一个值得注意的地方,代码中有两段一模一样的代码段,执行结果却是一次逃脱成功,一次失败。这是因为任何一个对象的finalize() 方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize() 方法不会再被执行,因此第二次逃脱行动失败。

(4)有关finaliza()方法的建议

需要特别说的是,上面关于对象死亡时finalize() 方法的描述具有悲情色彩,作者并不建议开发人员使用这种方法拯救对象。应当尽量避免使用它,因为它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所做的一个妥协。它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。

有些教材中描述它适合做“关闭外部资源”之类的工作,这完全是对此方法用途的一种自我安慰。finalize() 能做的工作,使用try-finally 或者其它方法都更适合、及时,所以作者建议大家可以忘掉此方法存在。



5 . 回收方法区

(1)垃圾收集

大多数人认为方法区没有垃圾回收,Java虚拟机规范中确实说过不要求,而且在方法区中进行垃圾收集的“性价比”较低:在堆中,尤其是新生代,常规应用进行一次垃圾收集可以回收70%~95%的空间,而方法区的效率远低于此。

方法区的垃圾收集主要回收两部分:废弃常量无用类。

(2)“废弃常量”的回收

回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,例如一个字符串“abc”已经进入常量池,但是无任何String对象引用常量池的此常量,也无其它引用此字面量,此时发送内存回收,“abc”常量会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也是如此。

(3)“无用类”回收的条件

判定一个常量是否是“废弃常量”比较简单,而判定一个类是否是“无用类”的条件较为苛刻,需同时满足以下3个条件:

  • 该类的所有实例已被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类的方法。

虚拟机可以满足以上3个条件的无用类进行回收,这里仅说“可以”,并非如同“对象”不使用了就必然回收。

(4)注意

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证方法区不会溢出。




这一章有关内容学习下来真是收货颇丰,对象存活判定的两种算法、引用的概念和方法区回收的判定,特别是两种算法,面试中经常涉及,读者需注意理解学习。

若有错误,欢迎指教~

时间: 2024-10-12 15:39:51

JVM高级特性与实践(二):对象存活判定算法(引用) 与 回收的相关文章

JVM高级特性与实践(一):Java内存区域 与 内存溢出异常

对于从事C.C++的开发人员而言,在内存管理领域,他们具有绝对的“权利”——拥有每个对象的控制权,并担负着每个对象生命周期的维护责任.而对于Java开发人员而言,在虚拟机自动内存管理机制的帮助下,无需为每一个创建new操作去配对 delete/free 代码,减少内存泄漏和内存溢出的问题,这些都交给了Java虚拟机去进行内存控制,但是正因如此,当出现相关问题时,若不了解JVM使用内存规则,就难以排查错误.接下来以此篇文章记录学习Java虚拟机内存各个区域概念.作用.服务对象以及可能产生的问题.

深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)PDF下载

网盘下载地址:深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)PDF下载 – 易分享电子书PDF资源网 作者: 周志明 出版社: 机械工业出版社 副标题: JVM高级特性与最佳实践 出版年: 2013-9-1 页数: 433 定价: 79.00元 装帧: 平装 内容简介 · · · · · · <深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)>内容简介:第1版两年内印刷近10次,4家网上书店的评论近4?000条,98%以上的评论全部为5星级的好评,是整个Java图书领域公

《深入理解Java虚拟机 JVM高级特性...》核心笔记

深入理解Java虚拟机 JVM高级特性与最佳实践(第二版) 核心笔记 JAVA 环境: JAVA虚拟机高级特性: 一:java内存区域与内存异常 一):运行数据区     1:程序计数器(Program Counter Register),也称"PC寄存器" A:用来指示需要执行哪条指令的.(在汇编语言中,CPU在得到指令之后,程序计数器便自动加1或者根据                    转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令.) B:由于在JVM中,多线程

JVM高级特性-三、垃圾收集之判断对象存活算法

一.概述 运行时数据区中,程序计数器.虚拟机栈.本地方法栈都是随线程而生随线程而灭的 因此,他们的内存分配和回收是确定的,在方法或线程结束时就回收.而Java堆和方 法区则是不确定的,程序运行过程中创建对象的大小是不定的,只有在程序处于运行 期才能知道所需内存的大小 二.“存活算法” 要判断对象是否存活,主要有两种算法:引用计数法和可达性分析算法 引用计数法 引用计数法就是给对象加上一个引用计数器,每当对象被 引用一次 计数器值就加1,引用时效则减1,计数器为0则表示不会再被使用. 可达性分析算

深入java虚拟机-jvm高级特性和实战

第一部分 走近java 第一章 java技术体系 官方所定义的java技术体系 java程序设计语言 各硬件平台上的java虚拟机 Class文件格式 java api类库 来自商业机构和开源社区的第三方java类库 我们可以把java程序设计语言.java虚拟机.java api类库这三部分统称为JDK,是用于支持java程序开发的 最小环境把java api类库总的javaSE api子集和java虚拟机统称为JRE,是支持java程序运行的标准环境. 第二部分 自动内存管理机制 第二章 内

性能优化之 JVM 高级特性

1.JVM体系结构 线程共享内存 可以被所有线程共享的区域,包括堆区.方法区.运行时常量池. 1.1 堆(Heap) 大多数时候,Java 堆是 Java 虚拟机管理的内存里最大的一块,所有的对象实例和数组都要在堆上分配内存空间,Java 对象可以分为两类,一类是快速创建快速消亡的,另一类是长期使用的.所以针对这种情况大多收集器都是基于分代收集算法进行回收. Java 的堆可以分为新生代(Young Generation)和老年代(Old Generation),而新生代(Young Gener

JVM高级特性-四、垃圾收集算法

一.标记-清除算法 标记清除算法是最基础的收集算法,执行过程就名字一样,分为两个阶段,标记和清除 首先对需要回收的对象进行标记,标记完成后统一对已标记对象进行回收,具体标记过程的介绍可以 看上一篇垃圾收集之判断对象存活算法中的"可达性分析"中介绍的

JVM高级特性-二、JVM在堆中对象的分配、布局、访问过程

前面介绍了jvm运行时数据区域后,下面讲解下对内存中数据的其他细节,看他们是如何创建.布局及访问的 一.对象的创建 对象的创建分配方式主要有两种:指针碰撞和空闲列表 指针碰撞: 假设堆内存中是绝对规整的,那么,在为新对象分配内存空间时,只需要将指针向空闲空间方向移动新对象所需大小的一段出来即可 空闲列表: 如果内存不是规整的,这时就需要维护一个列表,记录哪些内存是空闲的,在分配空间时,从列表中找出一块足够大的空间划分为对象实例并更新列表记录

挑战高薪必看:《深入理解java虚拟机 jvm高级特性与最佳实践》

Java是目前用户最多.使用范围最广的软件开发技术之一.Java 的技术体系主要由支撑 Java程序运行的虚拟机.提供各开发领域接口支持的Java API.Java 编程语言及许多第三方 Java框架(如Spring.Struts 等)构成.在国内,有关Java API.Java语言语法及第三方框 架的技术资料和书籍非常丰富,相比之下,有关Java虚拟机的资料却显得异常贫乏. 资料获取方法 内容特色 第一部分走近 Java 本书的第部分为后文的讲解建立了良好的基础.尽管了解Java技术的来龙去脉