这一部分的主题主要围绕以下三个问题展开探讨,学习:
1)哪些内存需要回收?
2)什么时候回收?
3)如何回收?
判断对象存活
引用计数算法
给对象一个引用计数器,每当有引用计数器就加一,失效就减一;任何时刻计数器不为零的时候就不能再使用。
这个算法效率高,但是很难解决循环引用的问题。所以java虚拟机没有采用改算法判断对象是否存活。
可达性算法
当一个对象到GC Roots对象没有任何的引用链相连时,则证明此对象是不可用的。
在java语言中,可作为GC Roots 的对象包括下面几种:
1)虚拟机栈(栈帧中的本地变量表)中的对象引用
2)方法区中类静态属性引用变量
3)方法区中常量引用的变量
4)本地方法栈JNI(一般说的是Native方法)引用对象
再谈引用
在jdk1.2之后对引用概念进行了扩充,分为强引用,软引用,弱引用,虚引用。
强引用:只用引用还在,垃圾收集器就不会收集这些对象,即使抛出OOM异常也不会去回收这些对象。
软引用:如果内存不够了,将要发生内存溢出之前,会把这些对象回收。
弱引用:不管内存够不够,只要垃圾收集器工作了,就会回收这部分引用占用的内存。
虚引用:它是最弱的一种引用,无法从虚引用获取对象实例,它唯一存在的目的就是能在这个对象被收集器回收时收到一个系统通知。
生存还是死亡
对象没有了GC Roots引用链并不是马上进行对象回收,而是会去标记,并进行筛选,筛选的条件就是对象是否需要执行finalize方法。如果有需要则将对象放到F-Queue队列里面去,等待执行finalize方法执行。
回收方法区
方法区主要回收两部分的内容:发起常量和无用的类。回收废弃常量与回收java堆中的对象非常类似,没有任何对象引用该常量,那么这个常量就会被清理出常量池。
判断一个类是否是无用类比较苛刻,需要满足下列三个条件:
1)该类的所有实例都被回收,java堆不存在该类的任何实例
2)加载该类的ClassLoader已经被回收
3)该类对应的java.lang.Class 对象没有在任何地方被引用,无法再任何地方通过反射访问该类。
垃圾收集算法
标记-清除算法
先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。缺点:
1)效率不高。标记和清除的效率都不是很高。
2)标记清除之后会产生大量不连续的碎片。
复制算法
目前商用的虚拟机都采用这种算法来回收新生代。将内存分为一块较大的(80%)的Eden空间,和两块较小的Survivor(10%)空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性的复制到另一块的Survivor空间上,最后清理刚刚用过的Survivor和Eden空间。这样的话就只用百分之十的内存被浪费掉,如果还存活的对象所需的空间大于剩下的10% 的空间,则想老年代进行分配担保。
标记-整理算法
标记过程和”标记-清除“算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都往一边移,然后清理可回收的对象。
分代回收算法
将java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。在新生代中每次垃圾收集都有大批的对象死去,只有少量存货所以采用复制算法。而老年代因为存活率比较高,所有采用的是”标记-清除” 或者是”标记整理”算法
hotspot算法实现
枚举根节点
由于多线程下,可能导致对象的引用不断的发生变化,所以在执行GC 的时候,必须停顿所有的java执行线程。
在HotSpot的实现中,是使用一组称为OopMap 的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置,记录下栈和寄存器的引用,这样GC在扫描的时候就能直接知道对象的引用信息了。
安全点
程序执行并非在所有的地方都能停顿下来开始GC,只有到达安全点才能暂停,安全点的选择基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定。
让所有线程在GC发生的时候都停下来,有两种方案选择:抢先式中断和主动式中断。
抢先式中断:直接把所有线程中断,如果发现有线程中断地方不在安全点上面,就恢复线程,让它跑到安全点上面。目前几乎没有虚拟机采用抢先式中断方式来执行GC。
主动式中断:设置一个标志,让所有线程主动去轮询这个标志,发现中断标志之后就自己中断挂起。
安全区域
安全区域是安全点的扩展,线程执行都Safe Region中的代码时,首先标识自己已经进入Safe Regin ,那样,当JVM执行GC的时候,就不用管Safe Region 状态的线程了。在线程要离开Safe Region 的时候,它要检查系统是否已经完成了GC,如果完成了,线程继续执行,如果没有完成,线程就会停下来等待GC执行完毕。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
serial 收集器
它是最基本历史最悠久的收集器(在JDK1.3.1之前是新生代收集器的唯一选择),它只会使用一条线程去收集垃圾,收集的区域是收集新生代的内存区域。而且在执行GC 的时候其他线程必须得停下来(Stop the world) 。虽然收集的时候得完全暂停其他线程,但是只要不是很频繁发生,Serial收集器对于运行在Client模式下面的虚拟机来说是一个不错的选择。
parnew收集器
parnew收集器其实就是Serial收集器的多线程版本。除了使用多条线程进行垃圾收集之外,其余行为还包括Serial收集器可用的所有控制参数。可以通过
-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
parallel scavenge收集器
他是一个新生代收集器,使用的也是复制算法的收集器。Parallel Scavenge收集器目标是达到一个可控制的吞吐量,所谓吞吐量就是用户代码运行时间于CPU总消耗时间的比值。它提供了两个参数用于精确的控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的 -XX:GCTimeRatio参数。
如果用户对JVM不了解,不熟悉,该垃圾收集器则会自适应调节各个参数,做到自适应调节。
serial old 收集器
Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用的是“标记-整理“算法。
parallel old 收集器
Parallel Old是 Parallel Scavenge 收集器的老年代版本。 使用多线程和”标记-整理“算法实现,这个收集器是jdk1.6才开始提供的,在注重吞吐量敏感以及CPU资源敏感的场合都可以考虑Parallel Scavenge 和Parallel Old收集器。
cms 收集器
CMS(concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。如果希望系统停顿时间最短,CMS收集器就非常符合这类应用的需求。
使用的算法是”标记-清除“算法。它的运作过程包含四个步骤,做到了并发收集,低停顿,回收的内存区域是老年代的内存区域。
1)初始标记
2)并发标记
3)重新标记
4)并发清除
虽然有这些特点,但是CMS还不算完美,有一下缺点:
1)CMS收集器对CPU资源非常敏感。
2)CMS无法处理浮动垃圾,可能出现”Concurrent Mode Failure“ 失败导致另一次Full GC的产生。
3)CMS采用的垃圾收集算法”标记-清除“算法,收集结束时候将产生大量的空间碎片,空间碎片的存在将会给大对象分配带来麻烦,当老年代空间碎片过多,没有连续的空间导致无法分配给大对象,会导致一次Full GC,为解决这个问题,在碎片过多导致无法分配给大对象空间时候,CMS会合并整理剩余的空间,使用参数 -XX:+UseCMSCompactAtFullCollection来配置(默认开启)
g1收集器
G1(Garbaage-first)收集器是当今收集器技术最前沿的成果之一,它被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。与其他收集器相比,G1具备以下特点:
1)并行并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间
2)分代收集:与其他收集器一样,分代概念在G1中依然保留。
3)空间整合:基于复制算法 整合”标记-整理“算法的特点,提供规整的回收后的内存空间。
4)可预测的停顿:这是G1相对于CMS的另一大优势,G1可建立可预测的停顿时间模型,能让使用者明确制定一个长度为M毫秒的停顿时间。
在G1收集器之前的其他收集器收集的范围都是整个新生代或者老年代,而G1不再是这样,使用G1收集器时,java堆内存布局就与其他收集器有很大区别,它将整个java堆划分为多个大小相等的独立区域,虽然还保留新生代老年代的概念,但是新生代和老年代不再是物理隔离,他们是一部分Region的集合。
垃圾收集器参数总结
内存分配与回收策略
java技术体系中所倡导的的自动内存管理最终可以归纳为自动化的解决两个问题:给对象分配内存以及回收对象内存。
1)对象优先分配在Eden分配
大多数情况下,对象在新生代Eden区分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次 Monitor GC。
2)大对象直接进入老年代
所谓的大对象是指,需要大量的连续内存的空间的java对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机来时是一个坏消息,比遇到一个大对象更坏的消息就是遇到一群 朝生夕死的”短命大对象“,经常出现大对象容易导致内存还有不少空间的时候就提前触发垃圾收集以获取足够的空间来安置他们。
3)长期存活的对象将进入老年代
虚拟机为每一个对象定义了一个对象年龄计算器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移到Survivor空间中,并且对象的年龄添加一岁,当他的年龄增加到一定的程度(默认是15岁),就会晋升到老年代,对象晋升到老年代的阀值可以通过 -XX:MaxTenuringThreshold设置
4)动态对象年龄判定
为了能更好的适应不同程序的内存状况,虚拟机并不是永远要求对象年龄达到了MaxTenuringThreshold 才进入老年代,如果在Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold的阀值年龄要求。
5)空间分配担保
在Monitor GC之前,虚拟机会检查老年代最大的可用连续空间是否大于新生代所有对象总空间,如果是,那么Monitor GC确认是安全的,如果不是,虚拟机会查看HandleProotionFailure设置是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试执行一次GC,尽管这次GC是有分风险的,如果小于,或者HandleProotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
小结
内存回收与垃圾收集在很多时候都是影响系统 性能、并发能力的主要因素之一,虚拟机之所有提供多种不同的收集器以及提供大量的调节参数,是因为止只有根据实际应用需求、实现方式选择最优的手机方式才能获取最高的性能。