java虚拟机的生命周期:
启动一个java程序时,一个虚拟机实例开始诞生,程序关闭的时候,虚拟机实例也随之消亡。
对象死亡判断方法
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
1、引用计数算法:
- 给对象增加一个计数器
- 对象每被引用一次,计数器+1,计数器数值越大说明对象引用越频繁。
- 每当引用失效,计数器 -1;
- 任何时候计数器为0的对象就是不可能再被使用的
优点:算法简单,效率高
缺点:不能帮助解决相互引用对象的回收问题。
所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
2、可达性分析
这个算法的思想是通过一系列称为“GC ROOTs”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC ROOTs没有任何引用链的话,就证明这个对象是不可用的,需要被回收。
分析过程
在垃圾回收器进行垃圾回收时,它首先从一组GC ROOTs开始遍历这些根对象可以访问到的对象引用,并构建出一个存活对象的图。然后遍历这个对象图,标记所有可达的对象。所有未被标记的对象都被视为不再使用的对象,可以被回收。
哪些对象可以作为GC ROOTs
- 虚拟机栈(栈帧中的局本部变量表)中引用的对象
- 本地方法栈(Native方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中类常量引用的对象
- 所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
对象可以被回收就代表一定会回收吗?
即使在可达性分析中不可达的对象,也并非“非死不可”的,这个时候它们正处于“缓刑阶段”,要真正宣告一个对象的死亡,至少要经过两次标记过程,可达性分析中不可达的对象被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没必要执行。
被判定为需要执行的对象会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会真的被回收。
finalize方法在JDK 9及后续版本被舍弃。
Java引用类型
无论是“引用计数法”判断对象引用数量还是“可达性分析”判断对象的引用链是否可达,判断对象的存货都与“引用”相关。
在JDK 1.2 之后 Java 对引用的概念进行了扩充,将引用分为以下四类:
- 强引用(Strong Reference)
- 软引用(Soft Reference)
- 弱引用(Weak Reference)
- 虚引用(Phantom Reference)
强引用
Java里的引用默认就是强引用,任何一个对象的赋值操作对产生了这个对象的强引用,例如:
Object obj = new Object();
强引用的特性是,只要有强引用存在,被引用的对象就不会被垃圾回收。
软引用
软引用是由java.lang.ref.SoftReference所提供的功能,意思是只有在内存不足的情况下,被引用的对象才会被回收。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
弱引用
弱引用是由java.lang.ref.WeekReference所提供的功能,不同的是weekReference引用的对象只要垃圾回收执行,就会被回收,而不管是否内存不足。
虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。虚引用是由java.lang.ref.PhantomReference所提供的关联功能。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
垃圾收集算法
1.标记-清除算法
分为“标记”和“清除”两个阶段
第一步:发生垃圾清理操作时,将可以被回收的对象进行标记处理。
第二步:回收第一步中标记的对象所占的空间。
优点:算法简单。
缺点:标记和清除两个过程效率都不高;内存碎片化严重,后续可能会发生大对象找不到可以存放的连续内存空间问题。
2.复制算法
将内存分为大小相同的两块,每次只用其中的一块;当使用的这块内存存满后,将其中还存活的对象复制到另一块内存中,再将之前那块内存进行清理操作。
优点:不容易产生内存碎片。
缺点:内存被压缩到了原来的一半,如果存活对象比较多的话,复制算法效率会下降(不适合老年代)。
3.标记-整理算法:
根据老年代的特点提出的一种标记算法,标记过程与标记清除算法一样,但后续不是直接对可回收对象回收,而是让所有存活对象向内存的一端移动,然后清理掉端边以外的内存空间。
第一阶段:将需要清除的对象进行标记。
第二阶段:将存活的的对象(未被标记的)向内存的一端进行移动,移动完毕后,清除剩下的内存空间。
因为多了整理这一阶段,所以效率也不是很高,适合老年代这种垃圾回收频率不是很高的场景。
4.分代收集:
该算法是目前大部分JVM所采用的的一种垃圾回收算法,根据对象存活周期的不同将内存分为几块。一般将Java堆分为新生代和老年代,这样可以根据各个年代的特点使用不同的垃圾收集算法。
新生代 :Eden、S0、S1
一般新生代会划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间。
Eden空间比Survivor0和Survivor1空间大,这是因为在新生代中大部分的对象被创建后很快就会被GC,所以Survivor空间不用分配太多的空间。
大部分情况,对象首先会在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,就会进入Survivor 0 或 Survivor 1,并且对象的年龄会+1,当它的年龄达到一定程度(默认15)就会被晋升到老年代。
当进行回收时,将Eden空间和Survivor0空间中还存活的对象复制到另一块Survivor1空间中,
然后清除Eden和Survivor0的空间,之后就使用Eden和Survivor1这两块空间。
当进行下一次垃圾回收时,将Eden和Survivor1的空间还存活的对象复制到Survivor0,然后清除Eden和Survivor1的空间。
以此类推,每次总有一个Survivor空间是空的。即新生代一般使用复制算法。
当然,如果目标Survivor空间无法存储Eden和Survivor存活的所有对象时,会将这些对象存储到老生代。
当对象中Survivor区躲过一次GC时,其年龄就会加1,当到达一定的年龄后,会将其移动到老生代。
每次垃圾回收新生代大部分对象被回收老生代少部分对象被回收。
老年代:Tenured
在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老生代中。
因此,可以认为老生代中存放的都是一些生命周期较长的对象。所以在老生代中一般所采取的垃圾回收算法是标记-整理算法。