我们知道Java内存运行时数据区域划分为5个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生,随着线程而灭;栈中的栈帧也随着方法执行结束而出栈;这几个区域因为方法结束或者线程结束它们的内存自然就回收了。
而Java堆和方法区,只有程序处于运行期间时才会知道创建那些对象,这部分的内存分配与回收是动态的,是垃圾收集器所需要回收的区域。
在回收对象之前,首先要确定对象能否被回收。当一个对象不在被其它途径使用时,这个对象就可以被回收了。
一、垃圾标记算法
在堆里面存放着Java中几乎所有的对象实例,垃圾回收器在对堆进行回收前,首先需要判断对象是否是垃圾。
判断一个对象是否是垃圾通常有两种算法:引用计数器算法、可达性分析算法
1.1、引用计数器算法
给对象添加一个引用计数器,每当有一个地方引用他时,计数器的值就会加1;当引用失效时,计数器的值就减1;当计数器的值为0时,表示这个对象不在被引用,可以被回收。
缺点:
- 无法解决对象间相互引用的问题
public class ReferenceGC {
private ReferenceGC instance = null;
public static void main(String[] args) {
ReferenceGC obj1 = new ReferenceGC();
ReferenceGC obj2 = new ReferenceGC();
obj1.instance = obj2;
obj2.instance = obj1;
obj1 = null;
obj2 = null;
}
}
上面代码中 obj1 与 obj2 之间存在相互引用,即使将obj1和obj2 置为null,它们各自的引用计数器的值依然为1,垃圾回收器会认为它们依然在被引用,而不会回收它们。
因此目前Java中的垃圾回收器都没有使用引用计数器算法来判断一个对象是否是垃圾,而是采用可达性分析算法
1.2、可达性分析算法
这个算法的基本思想就是通过一系列称为 GC Roots
的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(就是GC Roots到这个对象不可达)时,则证明此对象是不可用的。
如下图所示,对象obj5、obj6、obj7 虽然互相 有关联,但是它们到GC Root 是不可达的,所以它们将会被判为可回收的对象。
在Java中,可作为GC Root 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
二、垃圾收集算法
1、标记 – 清除算法
“标记-清除”(Mark-Sweep)算法是最基础的收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,标记过程使用垃圾标记算法。后面其它算法都是基于这种思路对其不足进行改进得到的。
缺点:
- 效率问题:标记和清除的效率不高
- 空间问题:标记清除过后会产生大量不连续的内存碎片。空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
标记除算法执行过程如下图:
2、复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了。它将可用内存按容量划分为大小相等的2块,每次只使用其中一块。当其中一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:
实现简单、在对象存活率较低的情况下效率很高
缺点:
每次只能使用其中一半内存
现在商用JVM都采用这种收集算法来回收新生代,因为新生代的对象基本上都是朝生夕死的,存活下来的对象很少约占10%,所以不需要按照1:1来划分内存空间,而是将 新生代 划分为一块较大 Eden 空间和 两块较小Survivor 空间,Survivor空间又划分为 From Survivor 和 To Survivor,Eden和两块Survivor空间内存划分比为8:1:1。
每次只使用Eden空间和其中一块Survivor空间。当回收时,将Eden区和Survivor中还存活着的对象一次性的复制到另一块Survivor空间上,最后清理掉Eden和刚刚使用过的其中一块Survivor区,因此每次新生代中可用内存为新生代总容量的90%,只有10%会被闲置,而当Survivor空间不够使用时,需要依赖老年代进行分配担保。
3、标记 – 整理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以老年代一般都不使用复制算法。
“标记-整理”(Mark-Compact)算法:
首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,最后直接清理掉边界以外的内存。
4、分代收集算法
当前商业虚拟机都采用分代收集算法,根据对象存活周的不同,将内存划分为几块。一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点,采用适当的收集算法。
在新生代中,每次垃圾收集时都会有大量对象死去,只有少量存活,就选用复制算法。
而老年代中因为对象存活率较高、没有额外内存空间对它进行分配担保,就必须采用标记-清除或标记-整理算法对它进行收集。
图片引用:https://www.cnblogs.com/vipstone/p/10316002.html
三、内存分配与回收策略
Java自动内存管理机制的目的:
- 给对象分配内存
- 回收分配给对象的内存
3.1、对象优先分配在Eden区上
大多数情况下,对象在新生代的Eden区上分配。当Eden区上没有足够的空间进行分配时,虚拟机将触发一次Minor GC。
新生代可用的空间为90%(Eden区+1个Survivor区)。
3.2、大对象直接进入老年代
大对象指:需要大量连续内存空间的Java对象。最典型大对象就是很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发GC来获取足够的连续空间来安置它们。
3.3、长期存活的对象进入老年代
虚拟机采用的分代收集思想管理内存,为了能够识别新生代和老年代,虚拟机给每个对象定义了一个对象年龄(Age)计数器。
如果对象在Eden区出生并经历过第一次Minor GC后仍然存活,并且能被Survivor容纳话,将被移动到Survivor空间中,并且对象的年龄设定为1。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当他的年龄增加到一定程度(默认15岁),将会被移动到老年代中。
3.4、动态对象年龄判断
为了能更好的适应不同程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenruingThreshold才能晋升老年代。
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenruingThreshold中要求的年龄。
3.5、空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间.
如果这个条件成立,那么MinorGC可以确保是安全的。
如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于或者HandlePromotionFailure设置不允许冒险,那这时需要改为进行一次FullGC。