天天看点

Java GC系列之垃圾回收机制

简述

垃圾回收-GC(Garbage Collection)。

在讲垃圾回收之前,我们需要思考一个问题:wwh(which、when、how)

哪些内存需要回收,什么时候回收、怎么回收?

java内存区域分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。

which:其中,本地方法栈、虚拟机栈、程序计数器3个区域跟线程“同生共死”,线程结束,内存也就回收了。而堆与方法区则不一样,程序在运行期间可能会创建对象或者常量等,因此这部分的内存的分配和回收都是动态的,所以,垃圾回收器主要在这部分内存运行。

when:堆中存放中程序里面几乎所有的对象实例,当对象“死”了以后,这部分内存就应该回收掉,那么怎么判断对象是否“死”了呢?

对象死了吗?

java垃圾收集算法一定是回收的已经“死”了的对象,判断对象是否存活java有几种算法机制。

1.引用计数器:给对象添加一个引用计数器,每被引用一次,计数器+1,引用失效,计数器-1,任何时候,只要计数器为0,对象就是不可能再被使用的,这时候我们认为这个对象已经“死”了。当然引用计数器存在一定弊端,两个都未被使用的对象相互引用对方,那么它们的计数器一直为1,有可能会引起内存泄露。(这也是主流java虚拟机不选用此方法的主要原因之一)

2.可达性分析:这种算法的基本思路是通过一系列的“GC Roots”的对象作为起始点,从这些起始点向下搜索,搜索路径称为引用链,当一个对象到“GC Roots”没有任何引用链时,我们称之为“不可达”,即,这个对象时不可用的。

Java GC系列之垃圾回收机制

在java语言汇总,可以作为“GC Roots”节点的对象包含以下几种:

1.虚拟机栈(栈帧中的本地变量表)中引用的对象。(虚拟机栈中所有未弹栈的对象)

2.方法区中类静态属性引用的对象。(方法区是线程共享的,因此方法区的静态对象(被static修饰)可作为节点)

3.方法区中常量引用的对象。(同上,常量池里面的对象(被static final修饰)也可以作为节点)

4.本地方法栈中JNI引用的对象。(引用native方法的对象)

对象死亡之最终判定

通过上面的可达性分析,即使对象时“不可达”的,对象也并非“非死不可”,这个时候,它们处于“缓刑”阶段,要宣判“死刑”至少还需要进行两次“审判”,即,要宣布一个对象真正死亡,至少还要经历两次标记过程。

如果对象进行可达性分析后判定它是“不可达”的,那么它将会被第一次标记并进行筛选.(筛选条件:对象是否有必要进行finalize()方法,如果对象没有覆盖finalize()方法,或者finalize()方法已经被调用过,虚拟机将这两种情况都视为“没有必要执行”。如果对象有必要执行finalize()方法,那么这个对象会被放在F-Queue队列中,等待触发finalize()方法,如果对象覆盖了finalize()方法并且在方法中与任一引用链上的对象建立关联【比如把自己(this)赋值给某个类变量或者某个对象的成员变量】,那么第二次标记的时候他就会被移除出队列,如果对象第二次标记还没有逃脱,那么就真的死亡了。)

注意:任何一个对象的finalize()方法只会被调用一次,因此只能逃脱一次。

how:ok,重点来了,如何去回收或者说以怎样的机制去回收是本次讲的重点了。由于java虚拟机规范中说过不要求在方法区实现垃圾收集(性价比低)因此,主要讨论堆中垃圾收集。

堆内存垃圾回收

java堆内存区域可大致分为:新生代、老年代、永久代。(永久代在java8后从堆中移除,移到直接内存区域,也就是“元空间”中)

其中新生代一次垃圾收集一般可以回收“70%~95%”的空间。

新生代:占1/3的堆空间。(对象“朝生夕死”)

老年代:占2/3的堆空间。(对象一般活得比较久)

新生代中又可以细分为Eden区、Survivor区(SurvivorTo、SurvivorFrom)。

eden:survivorto:survivorfrom = 8 : 1 : 1

垃圾收集算法

1.新生代

标记-清除算法:

最基础的收集算法——“标记-清除”算法,首先标记出所有需要回收的对象,在标记完成后同意回收所有被标记对象,对象标记过程以及判定上面已经说过了。

缺点:标记和清除过程效率低,标记清除之后会产生大量不连续内存碎片,程序分配大对象时,无法找到足够的连续内存,不得不提前触发GC。

标记整理算法:

标记过程与“标记-清除”一样,不过标记完成后不会直接清理,而是将所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

Java GC系列之垃圾回收机制

复制算法:

将可用内存划分为大小相等的两块,每次只使用其中一块,当一块内存用完,就将存活的对象复制到另一块上面,然后将已使用过的一次清理掉。

优缺点:实现简单,运行高效,但内存缩小为原来一块,可用空间为50%。

改进并商用:

现代商用虚拟机都采用“复制”算法来回收新生代,新生代对象“朝生夕死”,因此内存没有必要划分为两块同等大小(1:1),而是将新生代内存空间划分为一块较大的Eden空间和两块较小的Survivor空间(HotSpot虚拟机默认8:1:1),每次使用Eden和其中一块Survivor,当回收时,将还存活的对象复制到另外一块Survivor中,最后清理掉Eden和刚用过的Survivor。这样每次新生代可用空间为90%。

如果回收时,超过了10%的对象存活下来,那么未使用的那块Survivor是不够用的,这就需要老年代进行分配担保,对象直接进入老年代。

分配担保:

在发生Minor GC之前,虚拟机会检查老年代的最大连续可用空间是否大于新生代所有对象总空间,如果大于,那么Minor GC可以确保是安全的,如果小于虚拟机会查看HandlePromotionFailure设置是否允许担保失败,如果允许,则会继续检查老年代最大可用连续空间是否大于历次进入老年代对象的平均大小,如果大于,会进行Minor GC(有风险),如果小于或者设置了不允许担保失败,进行Full GC。

2.老年代:

采用“标记—清理”或者“标记—整理”算法进行回收。因为老年代对象存活几率大、时间长,使用复制算法则需要进行较多的复制操作,效率反而会降低低。

长期存活的对象进入老年代(对象每经过一次Minor GC并存活下来,年龄+1,达到15岁自动晋升到老年代)

大对象自动进入老年代(如:很长的字符串或者数组)

动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代

继续阅读