做者:fzsyw算法
一,概述缓存
Java运行时区域中,程序计数器,虚拟机栈,本地方法栈三个区域随着线程的而生,随线程而死,这几个区域的内存分配和回收都具有肯定性,不须要过多考虑回收问题。而Java堆和方法区则不同,一个接口的多个实现类须要的内存不同,一个方法的多个分支须要的内存可能也不一眼,咱们只有在运行期,才能知道会建立的对象,这部分的内存分配和回收,是垃圾回收器所关注的。垃圾回收器须要完成三个问题:那些内存须要回收;何时回收以及如何回收。安全
二,哪些垃圾须要回收并发
垃圾回收的基本思想是考察一个对象的可达性,即从根节点开始是否能够访问到这个对象,若是能够,则说明对象正在被使用,相反若是从根节点没法访问到这个对象,说明对象已经再也不使用了,通常来讲此对象就是须要被回收的。这个算法为根搜索算法。ide
2.1可达性分析优化
可是实际中,一个不可达的对象有可能在某种条件下“复活”本身,那么对它的回收就是不合理的。为此给出一个对象可达性状态的定义,并规定了在什么状态下能够安全的回收对象。可达性对象包含了如下三种状态。this
可达的:从根节点开始,按照引用节点,能够搜索到这个对象线程
可复活的:对象的全部引用都被释放,可是对象可能在finalize()方法中复活本身。对象
不可达的:对象的finalize()方法被调用,而且没有复活,那么就进入不可达状态。不可达的对象不可能会被“复活”,由于finalize()方法只能调用一次。blog
/** * * <p>Description: 1.对象被GC时,能够经过finalize拯救 2.finalize只被调用一次 </p> * @date 2019年8月25日 * @version 1.0 */public class FinalizeTest {
private static FinalizeTest currentObj;
@Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize invoke"); //从新引用 currentObj = this; }
public void alive() { System.out.println("live"); }
public static void main(String[] args) throws InterruptedException { currentObj = new FinalizeTest();
currentObj = null; System.gc(); //finalize优先级地,先等待 Thread.sleep(500); if(currentObj == null) { System.out.println("dead"); }else { currentObj.alive(); }
currentObj = null; System.gc(); //finalize优先级地,先等待 Thread.sleep(500); if(currentObj == null) { System.out.println("dead"); }else { currentObj.alive(); } }}
上面代码有一处同样的断码片断,可是获得的结果却并不相同,一次对象“拯救复活”成功,另外一次失败,那么就能够被正常回收。
能够做为GC Roots包括下面几种:
虚拟机栈(栈帧中的本地表量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用(即通常Native的方法)的对象
2.2 四种引用类型
在JDK1.2以后对引用进行了扩充,分为强引用,软引用,弱引用,虚引用4种,这四种强度一次减弱。经过对引用的扩充,能够依据内存的使用来描述这样的对象:当内存足够,则保留内存中;若是内存空间进行垃圾回收后仍是很紧张,则能够抛弃这类对象。不少系统的缓存功能符合这样的应用场景。
强引用
在Java中最多见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即便该对象之后永远都不会被用到JVM也不会回收。所以强引用是形成Java内存泄漏的主要缘由之一。
软引用
软引用须要用SoftReference类来实现,对于只有软引用的对象来讲,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用一般用在对内存敏感的程序中。
弱引用
弱引用须要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来讲,只要垃圾回收机制一运行,无论 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
虚引用须要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要做用是跟踪对象被垃圾回收的状态。
三,何时回收
按HotSpot VM的serial GC的实现来看触发条件主要分为如下几种:
young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,因此young GC后old gen的占用量一般会有所升高。
full GC:当准备要触发一次young GC时,若是发现统计数听说以前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(由于HotSpot VM的GC里,除了CMS的concurrent collection以外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,因此不须要事先触发一次单独的young GC);或者,若是有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。
HotSpot VM里其它非并发GC的触发条件复杂一些,不过大体的原理与上面说的其实同样。并发GC的触发条件就不太同样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen作并发收集。
四,如何回收
如何回收主要就涉及到垃圾回收的算法了。下面介绍几种垃圾回收算法的思想。
4.1 标记清除法(Mark-Sweep)
标记清除算法是现代垃圾回收算法的思想基础。它主要分为两个阶段:标记阶段和清除阶段。在标记阶段,首先经过根节点,标记全部从根节点开始的可达队对象,所以未被标记的对象就是未被引用的垃圾对象。而后在清除阶段,清除全部的未被标记的对象。
标记清除算法的不足有:效率的问题和标记清除后产生的大量不连续的内存碎片。而内存碎片太多可能会致使在分配大对象时,没法找到连续的内存而不得不提早触发另一次垃圾回收。
4.2 复制算法(Coping)
复制算法的核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中存活对象复制到未使用的内存块中,以后清除正在使用的内存块中的全部对象,交换两个内存的角色,完成垃圾回收。
若是系统中的待回收的对象不少,复制算法须要复制的存活对象就会相对较少,真正的垃圾回收时刻,复制算法的效率就会很高。并且对象是在垃圾回收过程当中的,统一复制到新的内存空间,再清除原来使用的内存,所以能够确保回收后的内存空间是没有碎片的。可是另外一方面,复制算法的代价是须要使用更多的内存空间。
复制算法比较适用于新生代。由于新生代垃圾对象一般多余存活对象,复制算法的效率会比较高。
4.3 标记整理算法(Mark Compact)
在老年代,大部分的对象都是存活对象。若是依然用复制算法,因为存活的对象多,复制的成本也将提升。所以基于老年代的垃圾回收特性,须要使用其余的算法。标记整理算法是一种老年代的回收算法。它在标记算法的基础上作了一些优化。和标记清除算法同样,它也是从更节点开始,可是并非清除未标记的对象,而是将存活的对象压缩到内存的一边,以后清除边界外全部空间。这种方法避免了碎片的产生,又不须要过多的内存空间,所以性价比比较高。
标记整理法的最终效果等同于标记清除算法执行完成后,再进行一次内存碎片的整理,所以也能够把它称为标记清除整理(MarkSweepComact)。
4.4 分代算法(Generational Collecting)
分代算法是根据对象存活周期不一样将内存化为几块。通常是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采用最合适的收集算法。新生代中的特色是对象朝生夕死,大约90%的新建对象会被回收,所以新生代适合复制算法。当一个对象通过几回回收后依然存活,对象就会被放入老年代的内存空间。在老年代中能够认为对象在一段时间内,甚至在程序的整个生命周期,是常驻内存的,能够对老年代使用标记清除和标记整理算法。
对于新生代和老年代来讲,一般新生代的回收频率很高,可是每次回收的耗时都很短,而老年代回收的频率比较低,可是会消耗更多的时间。
4.5 分区算法(Region)
通常来讲,相同条件下,堆空间越大,一次GC所须要的事件越长,从而产生的停顿也越长。为了更好的靠之停顿时间,将一块大的内存区域分割成多个大小形同的小区域,依据目标的停顿时间,每次回收若干个小区间,而不是整个堆空间,从而减小一次GC所产生的停顿。分区算法是将整个堆空间划分为连续的不一样小区间。每一个小区间独立使用,独立回收。