标记阶段的两种算法
对象存活判断
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
垃圾回收这个动作通过推理可知,至少需要两个步骤:1.判断对象是否是垃圾 2.回收垃圾 ;这里标记阶段就是第一步,判断对象是否是垃圾。
注意:
哪些地方有GC?
meta space 和 heap逻辑上是都有GC和OOM的,pc计数器没有GC,OOM,stack没有GC,有SOF。
方法区确实有GC,但是不是虚拟机必须清理的,所以这里说的GC主要说的是heap
引用计数算法
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型 的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:
实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一 条致命缺陷,导致==在Java的垃圾回收器中没有使用这类算法
注意
1.缺点1,2是可以推理到的
其实这种引用计数法的实现思路是很容易让大家想到的,即所有对象都维护一个变量存储当前对象被引用的次数,如果为0就标记为垃圾,但是这种方式必然两方面的确定:
1.开销内存空间
2.每次变量的重新赋值,对象应用的变化,都会导致,对象引用计数器的动态更新。
以上两点是可以直接推理出的缺点。
n2.循环引用
循环引用指的是一个变量P引用一个对象A,对象A的引用计数器属性为1,同时这个对象里面的一些变量,指向对象B,而对象B中有变量指向对象C,对象C中有变量指向对象A,这样就造成了一个循环引用,而此时对象A的引用计数器属性在原有的基础上+1,变成2。
一旦这个变量P不再指向对象A,那么此时其引用计数器属性变成了1,但是因为对象循环引用其引用计数器属性不会变成0,那么其也就不会被标记为垃圾,造成内存泄露。
例子:证明java没有使用引用计算
/**
* testGC()方法执行后,objA和objB会不会被GC呢?
* @author zzm
*/
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();
}
}
运行结果
[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]
Heap
def new generation total 9216K, used 82K [0x00000000055e0000, 0x0000000005fe0000, 0x0000000005fe0000)
Eden space 8192K, 1% used [0x00000000055e0000, 0x00000000055f4850, 0x0000000005de0000)
from space 1024K, 0% used [0x0000000005de0000, 0x0000000005de0000, 0x0000000005ee0000)
to space 1024K, 0% used [0x0000000005ee0000, 0x0000000005ee0000, 0x0000000005fe0000)
tenured generation total 10240K, used 210K [0x0000000005fe0000, 0x00000000069e0000, 0x00000000069e0000)
the space 10240K, 2% used [0x0000000005fe0000, 0x0000000006014a18, 0x0000000006014c00, 0x00000000069e0000)
compacting perm gen total 21248K, used 3016K [0x00000000069e0000, 0x0000000007ea0000, 0x000000000bde0000)
the space 21248K, 14% used [0x00000000069e0000, 0x0000000006cd2398, 0x0000000006cd2400, 0x0000000007ea0000)
No shared spaces configured.
从运行结果中可以清楚看到内存回收日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。
可达性分析算法
当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
即将活跃的引用都放到一组集合中,每个存放到集合中的引用都作为根节点即GC Roots,每个被引用的对象都通过一条线和GC Roots连接,那么只有在内存中被GC Roots连接的对象才是存活的对象。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
-
在虚拟机栈(栈帧中的本地变量表)中引用的对象
- 譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
-
在方法区中类静态属性引用的对象(1.7后存储在heap中)
- 譬如Java类的引用类型静态变量。
-
在方法区中常量引用的对象
- 譬如字符串常量池(String Table)里的引用。
在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
-
Java虚拟机内部的引用
- 如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
所有被同步锁(synchronized关键字)持有的对象。
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
-
比如:分代收集和局部回收(Partial GC)。
-
- 如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针 对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一.并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。比如:只回收new space 时需要将 old space中的指向也考虑进去。
上面列了那么多,其实就是想说,因为Root采用栈方式存放变量和指针,所以如果一个指针它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。将堆外,所有对于堆的指向都拷贝到GC Roots中。
注意
-
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在 一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
如同事物的一致性一样,即一个业务中操作,就是此时不能有其他业务操作改变GC Root 的连接。也就是保证每个GC Root的对象引用业务的一致性。(STW)
这点也是导致GC进行时必须“StopTheWorld"的一个重要原因。即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。