Java中引用和垃圾回收

很久之前就想写这么一个总结了,不过作为一个懒癌晚期换着,这个日志从创建到动手写几乎停留了快半年的时间。一方面,最初的时候感觉自己看的几篇介绍性的文章以及 Thinking in Java,只是知道有这么个东西,对它的理解其实不够;另一方面,自己确实没有实际的用过这部分内容(虽然现在还是没用过),已知感觉自己没太理解。终于在面试不顺,笔试被虐之后,决定静心整理些东西。

想最初学 C 的时候,每次 malloc 分配内存之后,需要手工 free 进行释放;C++里 new 了内存也需要 delete 回收。数据结构课程的时候,老师就强调,分配了一定要释放,不然要内存泄露。想来到了 Java、Python 等现代语言(233),这些底层的细节都已经不再是程序员关注的内容,因为垃圾回收!(原本想写成一篇读书笔记的,结果写成了一篇四不像……)

1、Java 中的引用类型

最初的最初,我以为 Java 只有一种引用类型。后来见到 WekaHashMap, 知道了WekaReference,再后来听说有四种引用类型,才去查资料知道了 SoftReference 和 PhantomReference。 其引用强度按列表顺序逐步降低:

  1. StrongReference:就是我们平常用的引用类型,gc 过程不会回收包含强引用的内存空间;如果显式的将内存的所有引用设为 null,或者超出对象的生命周期,gc 才会认为可以回收这部分空间。方法内部的强引用,保存在栈中,引用的对象保存在堆中。例如在 ArrayList 中,通过 elementData 数组保存 List 中的变量,在调用 clear() 方法时,需要将数组中的引用全部设为 null,可以及时释放内存。
  2. SoftReference:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存
  3. WeakReference:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 WeakReference 来记住此对象。 下面是一个例子:

    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

    当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。这个引用不会在对象的垃圾回收判断中产生任何附加的影响。

  4. PhantomReference: “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

一个表格总结以上四种引用:

引用类型 被垃圾回收时间    用途    生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 Unknown Unknown Unknown

2、如何确定一个对象是“垃圾”

引用计数

确定一个对象是否是“垃圾”,即判定某个对象是否可以被回收。在 Java 中,所有对象都是通过引用进行关联和操作的。那么最直观的方法就是利用引用计数来判断一个对象是否是“垃圾”:如果一个对象没有任何引用与其关联,那么这么对象就不会再被使用,即可以被回收。

但是引用计数存在一些问题:引用计数算法虽然简单、高效,但其过程穿插在整个程序运行过程中,每次对象的引用进行增加/减少时,都需要对计数进行操作。更为关键的是,它无法解决循环引用的问题

什么是循环引用?简单来说就是,A 对象 持有 B对象的引用,同时 B 对象持有 A 对象的引用。这种情况,当其他对 AB对象访问的引用失效后,其引用计数依然不为0,无法实现“垃圾回收”。

可达性分析

为了解决这个问题,在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。——参考链接2

直观上,就是将程序的引用之间以树的形式构造,从 GC Roots 开始遍历,所有遍历到的对象都不是“垃圾”,其余的对象均不可达,在 GC 运行时可以进行回收(还有待进一步确定)

一些 Java 基本概念

Java 中包括两种数据类型:基本类型和引用类型。这也是为什么总有人认为 Java 不是纯面向对象的语言,因为基本类型并不是一个对象

  • 基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress
  • 引用类型包括:类类型接口类型数组

栈(stack)和堆(heap):栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。为什么把这两部分分开呢:

  1. 从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
  2. 堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
  3. 栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
  4. 面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。

Java 对象的大小:第一次知道一个空的 Object 对象大小是 8 Byte,是在听 Coursera 上的《算法》课的时候。同样,一个引用的大小是 4 Byte,这 4 Byte是在栈中存储,8 Byte 是在堆中存储。同时一个 Java 对象大小,必须是 8 Byte 的倍数(猜测和内存对齐有关)。

典型的垃圾回收算法

典型的垃圾回收算法通常认为有4种:标记-清除(Mark-Sweep)、复试(Copying)、标记-整理(Mark-Compact)、分代收集(Generational Collection)

  • 标记-清除(Mark-Sweep)
  • 复试(Copying)
  • 标记-整理(Mark-Compact)
  • 分代收集(Generational Collection)

Python 中的垃圾回收

TODO

其他编程语言中的 GC 算法

其实我也不懂,虽然除了 Python、Java,还看过 Go、Ruby、Javascript(的语法),但是对这些都没有深入的钻研,附上知乎上一个回答吧:各种编程语言的实现都采用了哪些垃圾回收算法?这些算法都有哪些优点和缺点? 也许你看了这个问题的回答,就会吐槽看我这篇 Blog 浪费生命了。

参考链接:

  1. http://blog.csdn.net/mazhimazh/article/details/19752475 Java的引用类型
  2. http://www.importnew.com/19085.html Java 垃圾回收机制
  3. http://www.importnew.com/18694.html JVM 调优总结系列
  4. http://python.jobbole.com/83548/ Python 垃圾回收源码分析
  5. http://blog.csdn.net/yueguanghaidao/article/details/11274737 源码分析及 GC 模块详解
  6. http://www.houcj.net/blog/2015/05/10/garbage-collection-in-python/ Python 回收机制

Java中引用和垃圾回收》上有2条评论

  1. JY

    private transient Object[] elementData;
    public void clear() {
    modCount++;
    // Let gc do its work
    for (int i = 0; i < size; i++)
    elementData[i] = null;
    size = 0;
    }

    ==============
    请问这个例子中,如果设置的是elementData=null;的话,是不是各个elementData[i]作为引用还是也存在的?也就是说这个时候GC的话,elementData[i]的对象占据的内存就不会被回收了。。。

    回复
    1. Guo 文章作者

      我的感觉还是会被回收的。这个时候 elementData[i] 都是不可达的对象了。
      实验的结果是有时候会回收,有时候不回收(猜测不回收可能因为内存够用?)

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注