垃圾回收
通常垃圾回收是针对 Java 堆和方法区所做的操作,其他部分由于线程私有并且本身所占空间不大不用太关心,垃圾回收器要做的三件事:
-
哪些内存需要回收
在 Java 堆中,肯定是对那些不再被引用的对象实例进行回收,而如何判断对象不再被任何地方引用是个关键。通常有两种方式:- 引用计数法
实现简单,高效,但存在着相互循环引用的问题。 - 可达性分析
会有一个起始点,从该点出发进行搜索,能走到的对象就是可达的,也就是有效的引用,否则就是无效可回收的。
在 Java 中能作为起始点的有:Java 虚拟机栈的本地变量表中的引用对象,方法区中类静态属性引用对象,方法区中常量引用对象,本地方法栈中 JNI 引用对象。
在方法区中,主要回收废弃常量和无用的类。废弃常量的确定和确定 Java 堆中不再引用的对象实例类似。对于无用的类,则要满足 3 个条件才会被回收:
- 该类所有实例都已被回收
- 加载该类的 ClassLoader 已被回收
- 该类对应的 java.lang.Class 对象没有被任何地方引用,无法通过反射访问该类的任何方法。
虽说只要没有引用就会被回收,那怎么做到完全没有引用呢?Java 通过划分引用类型,控制着对象引用的不同。
- 强引用(Strong Reference)
默认的引用方式,通常用的也最多,回收器永远不会回收这类引用。 - 软引用(Soft Reference)
介于强引用和弱引用之间,在内存不足时会优先考虑。这种方式我觉得适用于 MVP 中的 V 层。 - 弱引用(Weak Reference)
引用对象只能生存到下次垃圾回收之前,不论内存是否足够,只要触发了垃圾回收,他们就会被回收掉。 - 虚引用(Phantom Reference)
用处不大。
- 引用计数法
-
什么时候回收
对象真正可回收需要经过两次标记。第一次是引用计数或可达性分析,第二次是对象如果有必要执行 finalize() 方法会进行第二次标记。如果在执行 finalize() 时没有救活自己,就准备被回收了。 -
怎么回收
几种基本的回收算法- 标记-清除算法
算法分为两个步骤,标记和清除,标记的过程就如上面所说,标记完之后,就会对标记区进行内存回收,这样内存空间就出来了。 - 复制算法
该算法是针对「标记-清除算法」效率低的问题进行的优化,将内存空间对半,每次仅用一半,当用完一半时,就将活着的对象复制到另一半去,然后回收这一半。
不过这是算法理论,实际上 IBM 将内存划分为 Eden 和 Survivor(该区域有两块),每次用 Eden 和其中一块 Survivor,另一块用来在回收时保存活着的对象。而HotSpot 虚拟机明确了 Eden 和 Survivor 的比例,8 : 1,即新生代的有效可用空间为 90 %,剩下 10 % 的空间用来保存对象。
但是 10 % 真的一定够吗?不够了怎么办,系统的设计是让老年代来做担保(类似于透支) - 标记-整理算法
该算法又考虑到「复制算法」在存活率较高的情况下,效率较低,而且很有可能出现担保现象的问题,对此进行了优化。这种算法更适合老年代(不然老年代找谁担保)。
这个算法分三个步骤,标记,整理,回收。标记过程和前面类似,标记完之后,会对存活的对象进行位置整理,使他们整齐排在一起,然后回收其他部分。
Java 堆根据对象存活周期的不同,划分为了新生代和老年代,不同代采用不同回收算法是比较合适的。通常新生代用「复制算法」,老年代用「标记-整理算法」或「标记-清除算法」
HotSpot 的算法实现
具体的算法实现肯定很复杂,这里仅引入几个概念。
在做可达性分析时会存在耗时和线程停顿执行的问题。为了解决这两个问题,引入了 OopMap 的数据结构,在类加载完时就存储好对象引用链相关的位置引用,这就大大提高了效率。
但 OopMap 不会都记下来,只有在「安全点」的时候才会存,「安全点」在我的理解是指线程执行指令到了某个逻辑划分的小任务完结点,此时存储一些数据是比较合适,并且这个时候做 GC 操作也是合适的。
那如何让线程走到「安全点」是个问题。系统采用「抢断式中断」和「主动式中断」对线程进行控制,现在主要以「主动式中断」为主,即在将要 GC 时,设置一个标记,各线程会主动查询这个标记,如存在这个标记,就把自己挂起。
「安全点」对醒着的线程是可靠的,但对于那些睡着的或者阻塞的线程,他们没办法知道那个标记点,于是设立一个「安全区域」扩大安全范围,线程进入「安全区域」时标记一下,当要出去时先看下外面安不安全,安全的话才能出去,否则就要等待可以出去的信号。
- 标记-清除算法
网友评论