JVM垃圾回收机制
哪些内存需要回收?
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了
栈中的栈帧随着方法的进入和退出就有条不紊的执行者出栈和入栈的操作,每一个栈分配多少个内存基本都是在类结构确定下来的时候就已经确定了,这几个区域内存分配和回收都具有确定性
而堆和方法区则不同,一个接口的实现是多种多样的,多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存也不一样,我们只能在程序运行的期间知道需要创建那些对象,分配多少内存,这部分的内存分配和回收都是动态的。
垃圾回收器关注回收的内存指的是堆和方法区。
判断对象存活
1.引用计数器法
给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的
缺点:不能解决互相引用时的问题。
2.可达性分析算法
通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的
Java语言中GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈JNI(Native方法)引用的对象
引用:
强引用就是在程序代码之中普遍存在的,类似Object obj = new Object() 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
软引用用来描述一些还有用但并非必须的元素。对于它在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存才会抛出内存溢出异常
弱引用用来描述非必须对象的,但是它的强度比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象
虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
什么时候回收?
在可达性分析算法中的不可达对象也不是一定会被回收,它还有一次拯救自己的机会(通过finalize方法)
垃圾回收的时候会进行两次标记,如果经过可达性分析以后发现没有与GC Roots相连接的引用链,那么它就会进行第一次标记然后进行筛选(筛选的条件是这个对象有没有必要执行finalize方法。当对象没有覆盖finalize方法的时候或者finalize方法已经被调用过一次,则是没有必要),如果是有必要的话,则这个对象会被放进F-Queue队列中,并在稍后被一个由虚拟机自动建立的,低优先级的finalize线程去执行,执行只是虚拟机触发,并不需要等他执行完,因为一个对象可能在finalize方法中执行缓慢,或者死循环,这样子可能那个队列的其他对象处于永久等待,则,整个垃圾回收崩溃。finalize是对象逃脱死亡命运的最后一次机会了,之后会对队列中的对象进行第二次标记,(如果对象成功的在finalize方法中于引用链建立连接,则会被移除即将回收的集合)如果不能成功拯救自己,则只能被回收了。
Finalize方法
任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。
回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类
废弃常量:假如一个字符串abc已经进入了常量池中,如果当前系统没有任何一个String对象abc,也就是没有任何Stirng对象引用常量池的abc常量,也没有其他地方引用的这个字面量,这个时候发生内存回收这个常量就会被清理出常量池
无用的类:
1.该类所有的实例都已经被回收,就是Java堆中不存在该类的任何实例
2.加载该类的ClassLoader已经被回收
3.该类对用的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
在大量使用发射,动态代理,CGLib,动态生成JSP及OSGi这类频繁自定义类加载器的场景需要虚拟机具备类卸载的功能,避免永久代溢出。
如何回收?
垃圾收集算法
标记—清除算法(mark-sweep)
算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象、
不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可
不足:将内存缩小为了原来的一半
实际中我们并不需要按照1:1比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor
当另一个Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代
标记整理算法(mark-compact)
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法
只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收
垃圾收集器
a)Serial收集器:
这个收集器是一个单线程的收集器,但它的单线程的意义不仅仅说明它会只使用一个COU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
优点:简单高效,没有线程交互的开销,可以专心做垃圾收集,所以效率高。(客户端)
b)ParNew 收集器:
Serial收集器的多线程版本,除了使用了多线程进行收集之外,其余行为和Serial收集器一样
并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上。
服务器端首选的新生代垃圾收集器。
c)Parallel Scavenge
是一个新生代收集器,它是使用复制算法的收集器,又是并行的多线程收集器。
吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
Parallel Scavenge 垃圾收集器的目标是达到一个可控制的吞吐量。
GC自适应调节策略(通过-xx:+UserAdaptiiveSizePolicy设置,之后不用手动指定新生代的大小比例和晋升老年代的对象大小了),是Parallel Scavenge 收集器和Parnew收集器的一个重要区别
而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收起停顿时间的
-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数
Parallel Scavenge收集器还有一个参数:-XX:+UseAdaptiveSizePolicy。这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGVPauseMillis参数或GCTimeRation参数给虚拟机设立一个优化目标。
自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别
新生代中的收集器只有Serial收集器和Parnew收集器可以一起和CMS合作。
d)Serial Old 收集器:
Serial收集器的老年代版本,是一个单线程收集器,使用标记整理算法
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
如果在Server模式下,主要两大用途:
(1)在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
Serial Old收集器的工作工程
e)Parallel Old 收集器:
Parallel Old是Paraller Seavenge收集器的老年代版本,使用多线程和标记整理算法
Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。
f)CMS收集器:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求
CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”.
CMS收集器主要优点:并发收集,低停顿。
CMS三个明显的缺点:
(1)CMS收集器对CPU资源非常敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种。所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想
(2)CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中蓝年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阀值已经提升至92%。
(3)CMS是基于“标记-清除”算法实现的收集器,手机结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间变长了。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标识每次进入Full GC时都进行碎片整理)
G1收集器的优势:
(1)并行与并发
(2)分代收集
(3)空间整理 (标记整理算法,复制算法)
(4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)
使用G1收集器时,Java堆的内存布局是整个规划为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在真个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的又来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽量可能高的灰机效率
G1 内存“化整为零”的思路
在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下步骤:
(1)初始标记
(2)并发标记
(3)最终标记
(4)筛选回收
HotSpot的算法实现:
1.枚举根节点(利用OoP的数据结构来达到虚拟机直接知道哪个地方存在对象引用。
2.安全点(在方法调用,循环跳转,异常跳转等指令才会产生安全点)
抢占式中断
主动式中断:设置标志
3.安全区域
理解GC日志
垃圾收集器中的参数总结
内存分配与回收策略
1.对象优先在Eden中分配
2.大对象直接进入老年代
3.长期存活的对象进入老年代
4.动态对象年龄判断
空间分配担保