javaGC与内存分配策略

在我们了解JavaGC之前,尝试思考3个问题:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

 

如何判定一个对象应该被回收?

  • 引用计数算法

很多教科书判断对象是否存活的算法是这样的,给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1,当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不再被使用的。

但,

主流的Java虚拟机没有选用引用计数法来管理内存的。

思考一个问题,如果有对象objA和objB,赋值objA.instance==objB.instance,除此之外没有其他引用。

则,

它们的计数器都不为0,于是引用计数算法 无法通知GC回收。

 

  • 可达性分析算法

在主流的商用程序语言的主流视线中,都是通过可达性分析来判定对象是否存活的。

这个算法基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即对象不可达时,证明此对象不可用。

如图中obj567,虽然相互关联,但是它们到GCRoot是不可达的,所以它们会被判定为可回收对象。

  • 在Java语言中,可作为GC Roots的对象包括下面几种:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中JNI(Native方法)引用的对象

 

生存还是死亡

即使在可达性分析算法中不可达的对象,也不是“非死不可”。要真正宣告一个对象死亡,要经历两次标记过程。

当可达性分析发现没有GC Roots引用链,将会第一次标记,并且进行一次“此对象是否有必要执行finalize()”的筛选。当对象没有覆盖finalize()方法,或已经被虚拟机调用过该方法,将被虚拟机视为没有必要执行。

如果对象有必要执行finalize()方法,将会被放置在F-queue队列中,稍后会由一个Finalizer线程去触发执行一次,并不承诺等待运行结束。然后GC将对Fqueue对象进行第二次小规模标记,然后才会被回收。也就是说如果对象需要拯救自己,则在finalize()中引用一个对象建立关联即可。但finalize并不会被执行第二次,所以对象只可能有机会自救一次。

在任何Java相关书籍中,均不推荐使用finalize()方法,因为他是不稳定的,C程序妥协产物。

 

垃圾收集算法

  • 标记-清除算法

最基础的算法,分标记和清除两个阶段。存在两个问题:1.效率问题;2.空间问题。

如图所示:

标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后运行过程需要分配较大对象时,无法找到足够的内存,而不得不提前触发另一次垃圾收集动作。

 

  • 复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。这样使每次都是对整个半区进行内存回收。分配时也不用考虑内存碎片等复杂情况。实现简单,运行高效。

这种算法的代价是将内存缩小为了原来的一半,代价较高。

现在的商业虚拟机都采用这种收集算法来回收新生代。

新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor。

大多数情况下,对象在新生代的Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC. 

HotSpot默认Eden和survivor比例是8:1,所以新生代内存分配中有90%的内存是被正常使用的。

当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保。如果没有足够空间存放存活对象,则这些对象将直接通过分配担保进入老年代。

 

  • 标记-整理算法

根据老年代特点,标记-整理算法标记过程与清除算法一样,然后让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

  • 分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期不同,将内存划分为几块。一般是把Java堆分为新生代和老年代,就可以根据各个年代的特点采用适当的手机算法。

如新生代中,每次垃圾收集都有大批对象死去,少量存活,就选用复制算法。

而老年代中因为对象存活率搞没有额外空间对他进行分配担保,就需要使用“标记-清理”或“标记-整理”算法来进行回收。

 

GC中的新生代与老年代

  • 对象优先在Eden分配

大多数情况下,对象在新生代的Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。

  • 大对象直接进入老年代

指需要大量连续内存空间的Java对象,最典型的就是很长的字符串和数组。

  • 长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一次MinorGC后仍然存活,并且能被Suevivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁。当增加到一定程度(通过MaxTenuringThreshold来设置,默认为15岁),就会被晋升到老年代中。

  • 动态对象年龄判定

如果在Survivor空间中年龄相同的所有对象大小总和大于Survivor空间的一半,年龄大于等于该年龄的对象直接进入老年代,不需要等到MaxTenuringThreshold中要求的年龄。

  • 空间分配担保

在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,MinorGC可以确保安全。

如果不成立,那么继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFailure不允许毛线,那这是要进行一次FullGC。

 

JVM(HotSpot) 7种垃圾收集器的特点及使用场景

新生代收集器:

1.Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器。是单线程的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。

 

Serial收集器依然是虚拟机运行在Client模式下默认新生代收集器,对于运行在Client模式下的虚拟机来说是一个很好的选择。

 

2.ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The Worl、对象分配规则、回收策略等都与Serial 收集器完全一样。

 

ParNew收集器是许多运行在Server模式下的虚拟机中首选新生代收集器,其中有一个与性能无关但很重要的原因是,除Serial收集器之外,目前只有ParNew它能与CMS收集器配合工作。

 

3.Parallel Scavenge(并行回收)收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器

该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收起停顿时间的

-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数

Parallel Scavenge收集器还有一个参数:-XX:+UseAdaptiveSizePolicy。这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGVPauseMillis参数或GCTimeRation参数给虚拟机设立一个优化目标。

 

自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

CMS不能与之共用。

 

老年代收集器

4.Serial Old 收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

如果在Server模式下,主要两大用途:

(1)在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用

(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

Serial Old收集器的工作工程

 

5.Parallel Old 收集器

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。

 

 

6.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时都进行碎片整理)

 

7.G1收集器

G1收集器的优势:

(1)并行与并发

(2)分代收集

(3)空间整理 (标记整理算法,复制算法)

(4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值