引用计数法
引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加一;引用失效,计数器就减一;任何时刻计数器为零的对象就是不可能被使用的。
但主流的Java虚拟机都没有选择引用技术算法进行内存管理,主要因为,这个算法有很多例外情况要考虑,必须配合大量额外处理才能保证正常的工作,例如:很难解决对象之间相互循环引用的问题。
例如:对于objA和objB都有字段instance,赋值令 objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已 经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也 就无法回收它们。
可达性分析算法
可达性算法:通过GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索所走过的路径被称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者说从GC Roots到这个对象不可达,就证明这个对象不能再被使用了。
Java体系里,可以作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象,例如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 方法区中类静态属性引用的对象,例如Java类的引用类型静态变量。
- 方法区中常量引用的对象,例如字符串常量池里的引用。
- 本地方法栈中JNI引用的对象。
- Java虚拟机内部的引用,如:基本数据类型对应的class对象,一些常驻的异常对象(比如 NullPointException、OOM等)还要系统类加载器
- 所有被同步锁(synchronized)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存。
除了这些固定的GC Roots集合之外,根据用户所选的垃圾收集器以及当前回收的内存区域不同,还会有其它对象“临时性”地加入,共同构成完整GC Roots集合。例如之后提到的分代收集和局部回收,如果只针对Java堆中某一块区域发起垃圾收集(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭的,所有某个区域里的对象完全有可能被位于堆中其它区域的对象所吸引,这时候就需要这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。
再谈引用
我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空 间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象。
所以Java将引用分为:强引用、软引用、弱引用、虚引用。这四种引用强度逐渐减弱。
- 强引用:是最传统的引用,程序代码中普遍存在的引用赋值,类似“Object obj = new Object()”这种引用关系。任何情况下强引用还存在,垃圾收集器就不会回收被引用的对象
- 软引用:一些还有用,但非必须的对象。被软引用关联的对象,在系统发送内存溢出异常前,就会把这些对象列进回收范围进行第二次回收,,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。使用SoftReference类来实现软引用。
- 弱引用:用来描述哪些非必须对象,但它的强度比弱引用还弱一些,被弱引用关联只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否够用,都会回收掉弱引用关联的对象。使用WeakReference类来实现引用。
- 虚引用:最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚函数引用关联的唯一目的是为了能在这个对象被收集器回收时受到一个系统通知。使用PhantomReference来使用虚引用。9+
生存还是死亡
被可达性算法判定为不可达对象后,这时它们处于“缓刑“阶段,要真正宣告一个对象死亡,
至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将第一次标记,随后进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,这种情况下我理解对象就要死了。
对象被判定为确有必要执行finalize()方法,那该对象会被放置在一个名为F-Queue的队列中,并由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。(这里说的执行是指虚拟机 会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因时,如果某个对象的finalize()方法执行的很慢,或者更极端发生了死循环,将很有可能导致F-Queue队列中的其它对象永久处于等待,或者导致整个内存回收子系统的崩溃。)finalize()方法是对象逃脱死亡的最后一次机会,之后收集器将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己,要重新与引用链上的任何一个对象建立关联,例如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那再第二次标记时它将被移除“即将回收“的集合。如果对象这个时候还没有逃脱,那他基本上就要被回收了。
finalize()方法只能被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会再被执行。
但这种方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐使用。finalize()方法所能做的所有工作,try-finally或者其它方式都能做的更好、更及时。
回收方法区
方法区:可以不要求虚拟机在方法区实现垃圾收集,垃圾收集性价比也通常较低。
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
回收废弃常量:回收废弃常量和回收Java堆中的对象十分相似。例子:假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接 口)、方法、字段的符号引用也与此类似。
判定一个常量是否“废弃”还是相对简单,但要判定一个类型是否属于“不再被使用的类”的条件就需要同时满足三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类和其任何派生子类的实例。
- 加载该类的类加载器已经被回收,(这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则很难达成。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java虚拟机会允许满足了这三个条件的无用类进行回收(和对象不一样,对象没有引用了一定会回收)