JVM虚拟机垃圾回收机制
JVM虚拟机垃圾回收机制
- 垃圾回收机制
-
- 判断是否存活算法
-
- 引用计数法
- 可达性分析法
- 最终判定
- 垃圾回收算法
-
- 分代收集机制
- 空间分配担保
垃圾回收机制
判断是否存活算法
java语言和我们之前学的c/c++不同,c/c++可以手动进行内存释放,那样随时随地就可以释放不必要的内存,减少内存溢出,但我们java的内存是有jvm虚拟机自行释放的,当一个对象不再被使用的时候,jvm会自行释放并回收内存,那么它是怎么样来判断一个对象是否存活的呢?想必这里一定有着一套耐人寻味的算法
引用计数法
1·每个对象都包含一个引用计数器,用于存放引用计数(其实就是存放被引用的次数)
2.每当有一个地方引用此对象时,引用计数+1
3·当引用失效(比如离开了局部变量的作用域或是引用被设定为null)时,引用计数-1
局部变量的作用域:从定义开始,到其所在的大括号结束为止。
4·当引用计数为0时,表示此对象不可能再被使用,因为这时我们已经没有任何方法可以得到此对象的引用了
但这种方法却存在一个问题,如果两个对象相互引用呢?
public class jvm {public static void main(String[] args) {Test a = new Test();Test b = new Test();a.another = b;b.another = a;a = b = null;}private static class Test{Test another;}
}
按照引用计数算法,那么当出现以上情况时,虽然我们无法在得到此对象的引用了,并且此对象我们也无需再使用,但是由于这两个对象直接存在相互引用的情况,那么引用计数器的值将会永远是1,但是实际上此对象已经没有任何用途了。所以引用计数法并不是最好的解决方案。
可达性分析法
首先每个对象的引用都有机会成为树的根节点(GC Roots),可以被选定作为根节点条件如下:
1.位于虚拟机栈的栈帧中的本地变量表中所引用到的对象(其实就是我们方法中的局部变量)同样也包括本地方法栈中JNI引用的对象。
2.类的静态成员变量引用的对象。
3.方法区中,常量池里面引用的对象,比如我们之前提到的String类型对象。
4.被添加了锁的对象(比如synchronized关键字)
5.虚拟机内部需要用到的对象。
一旦已经存在的根节点不满足存在的条件时,那么根节点与对象之间的连接将被断开。此时虽然对象1仍存在对其他对象的引用,但是由于其没有任何根节点引用,所以此对象即可被判定为不再使用。比如某个方法中的局部变量引用,在方法执行完成返回之后:
可以解释成当一个对象为null就是没有指向任何GC Roots则证明这个对象是不可以再使用的了
最终判定
我们在上面讲到的可达性分析法,他只能告诉我们jvm可以去回收,但真正的想回收还必须经过我们的最终判定,并不是说算法判定可以回收之后,这个对象一定会被回收
Object类里面有一个方法名为finalize(),这个方法就是最终判定方法,如果子类里面重写了此方法,放该子类对象被判定为可回收的时候,会进行二次确认,如果确认之后对象依然不满足被回收的条件的时候,那么该对象依然逃脱不了被回收的命运
示例代码:
public class jvm {private static Test a;public static void main(String[] args) throws InterruptedException {a = new Test();a = null;System.gc();Thread.sleep(1000);System.out.println(a);}private static class Test{@Overrideprotected void finalize() throws Throwable {System.out.println(this+"开启了他的");a = this;}}
}
运行结果:
但切记,该方法只会起到一次效果,如果之后再出现这种情况就不会奏效了
示例代码:
public class jvm {private static Test a;public static void main(String[] args) throws InterruptedException {a = new Test();a = null;System.gc();Thread.sleep(1000);System.out.println(a);a = null;System.gc();Thread.sleep(1000);System.out.println(a);}private static class Test{@Overrideprotected void finalize() throws Throwable {System.out.println(this+"开启了他的");a = this;}}
}
运行结果:
之所有代码中要加上Thread.sleep()是因为finalize()方法是新开了一个线程执行的,并且该线程的优先级比较低,以免主线程已经执行完了,该线程还在执行着
当然,除了堆中的对象以外,方法区中的数据也是可以被垃圾回收的,但是回收条件比较严格,这里就暂时不谈了
垃圾回收算法
前面我们介绍了对象存活判定算法,现在我们已经可以准确地知道堆中的哪些对象可以被回收了,那么,接下来就该考虑如何对对象进行回收了,垃圾收集器会不定期地检查堆中的对象,查看它们是否满足被回收的条件。我们该如何对这些对象进行回收,是一个一个判断是否需要回收吗?
分代收集机制
实际上,如冤我们对堆中的每一个对象都依次判断是否需要回收,这样的效率其实是很低的,那么有没有更好地回收机制呢?
第一步,我们可以对堆中的对象进行分代管理。
比如某些对象,在多次垃圾回收时,都未被判定为可回收对象,我们完全可以将这一部分对象放在一起,并让垃圾收集器减少回收此区域对象的频率,这样就能很好地提高垃圾回收的效率了。
因此,Java虚拟机将堆内存划分为新生代、老年代和永久代(其中永久代是HotSpot虚拟机特有的概念,在JDK8之前方法区实际上就是采用的永久代作为实现,而在JDK8之后,方法区由元空间实现,并且使用的是本地内存,容量大小取决于物理机实际大小
这里我们主要讨论的是新生代和老年代。
不同的分代内存回收机制也存在一些不同之处,在HotSpot虚拟机中,新生代被划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:∶ 1∶1,老年代的GC评率相对较低,永久代一般存放类信息等〈其实就是方法区的实现)如图所示:
Minor GC - 次要垃圾回收,主要进行新生代区域的垃圾回收
-触发条件:新生代Eden区容量已满的时候
Major GC -主要垃圾回收,主要进行老年代的垃圾收集
Full DC -完全垃圾回收,对整个java堆内存和方法区进行垃圾回收
·触发条件1:每次晋升到老年代的对象平均大小大于老年代剩余空间
·触发条件2: Minor GC后存活的对象超过了老年代剩余空间
·触发条件3:永久代内存不足(JDK8之前)
·触发条件4∶手动调用System.gc(方法
空间分配担保
考虑一种极端情况(正常情况下新生代的回收率是很高的,所以不用太担心会经常出现这种情况)就是当新生代的Eden区经历过一次回收之后,仍然存在大量的对象。
这时就需要用到空间分配担保机制了,可以把Survivor区无法容纳的对象直接送到老年代,让老年代进行分配担保(当然老年代也得装得下才行)在现实生活中,贷款会指定担保人,就是当借款人还不起钱的时候由担保人来还钱。
好,那既然新生代装不下就丢给老年代,那么要是老年代也装不下新生代的数据呢?这时,老年代肯定担保人是当不成了,那么这样的话,首先会判断一下之前的每次垃圾回收进入老年代的平均大小是否小于当前老年代的剩余空间,如果小于,那么说明也许可以放得下(不过也仅仅是也许,依然有可能放不下,因为判断的实际上只是平均值,万一这一次突然非常大呢),否则,会先来一次FullGC,进行一次大规模垃圾回收,来尝试腾出空间,再次判断老年代是否有空间存放,要是还是装不下,直接抛出0OM错误,摆烂。