垃圾收集

java与C++之间有一堵由动态内存分配和垃圾收集技术所围成的“高墙”。

1960年诞生于MIT的Lisp语言是第一门真正使用内存动态分配和垃圾收集技术的语言

垃圾回收 3大问题:

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

一、java内存回收的区域

在Java内存运行时区域的各个部分中:
程序计数器、虚拟机栈、本地方法栈3个区域(线程独立)随线程的创建而创建,随线程的销毁而销毁,每一个栈帧中分配多少内存基本上是在类结构确定下来就已知的,大体上可以认为是编译期可知。 因此这几个区域的内存分配和回收都具备确定性,不需要考虑回收问题,因为在方法或线程结束是,内存自然就回收了。
Java堆和方法区则不一样(是线程共享的),一个接口中的多个实现类可能需要的内存空间不同,一个方法中的多个分支也可能需要不同大小的内存空间,我们只有在程序运行期才能知道会创建那些对象,这部分的内存分配和回收都是动态的。

二、对象状态的判断

堆中存放着几乎所有的对象,垃圾收集器在对堆进行回收前会先判断对象是“存活”还是“死去”(即不可能再做任何途径使用的对象)

2.1引用计数算法(Reference Counting)

思想:在对象中添加一个引用计数器,没当有一个地方引用它时,计数器加1;失效时计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。
主流Java虚拟机并没有选用该算法管理内存

  • 优点:实现简单,判定效率高
  • 缺点:很难解决对象之间互相循环引用的问题
  • 应用:微软COM(Componentv Object Model)技术、使用ActionScript的FlashPlayer、Python语言等
2.2可达性分析算法(Reachability Analysis)

思想:通过一系列称为“GC roots”的对象作为起点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则证明该对象不可用。

GC Roots对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中(Native方法)引用的对象
  • 应用:Java、C#、Lisp等主流商用语言
2.3对象的引用概念

java1.2之后,Java对引用的概念做了扩充,将引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Rerference)4种,这4种引用强度依次逐渐减弱。

  • 强引用:指在代码中普遍存在的,类似new对象的方式的引用,只要这类引用还存在,垃圾回收器就永远不会回收掉被引用的对象
  • 软引用:用来描述一些还有用但并不必须的对象。对于软引用关联的对象,在系统发生内存溢出异常之前,会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够内存,则会抛出内存溢出异常。
  • 弱引用:用来描述一些不必须的对象。它的强度要比软引用更弱一些,被弱引用关联的对象只能存活到下依次垃圾回收之前。当垃圾收集器工作室,无论内存是否足够,都将回收被弱引用关联的对象
  • 虚引用:也被称为幽灵引用或者幻影引用,它是最弱的一种关联关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。设置虚引用的唯一目的就是,在这个对象被回收时能收到一个系统通知
2.4对象的死亡

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,要真正宣告一个对象的死亡,至少要经历两次标记过程:

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

如果该对象被判定有必要执行finalize()方法,那么这个对象将会防止在一个人叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的FInalizer线程去执行。这里的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,原因是如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,很可能会导致F-Quene队列中其他对象永久等待,甚至导致内存回收系统崩溃。

GC会对F-Quene队列中单的对象进行第二次小规模标记,如果对象要在finalize()中拯救自己,只要重新与引用链的任何一个对象建立联系即可(比如把自己赋值给某个类变量或对象的成员变量),那在第二次标记时它将被移除“即将回收”的集合。如果这时候对象还没有逃脱,那基本上它就真的被回收了。

值得注意的是,任何一个对象的finalize()方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。finalize()能做的工作,使用try-finally或其他范式都可以做到的更好、更及时。

三、回收方法区

方法区(永久代)并非是没有垃圾收集的,只是在方法区中进行垃圾收集的“性价比”一般较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

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

废弃常量:假如常量池中的一个字符串没有被系统在任何地方引用,此时发生了内存回收,在有必要的情况下,该字符串会被清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似

无用的类:

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

满足以上三个条件的无用类“可以”被回收,但并非和对象一样,不适用就必须被回收。

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