Java高级进阶 2 内存分配策略和垃圾收集

对象存活判断算法

    引用计数法

    给对象添加引用计数器,每当一个地方引用该对象,计数器+1,引用失效,计时器-1。任何时刻对象引用计时器为0时,该对象不可被使用。引用计数法实现简单、效率高,但是主流虚拟机都为采用该算法进行内存管理,因为该算法无法解决对象直接互相循环引用的问题。

    可达性分析算法

    目前主流虚拟机采用的主流算法,通过可达性分析来判断对象是否存活。可达性算法基本思路是提供一系列“GC Roots”的对象作为起始点,从这些节点往下搜索,搜索对象引用链,当对象到GC Roots没有任何引用链相连时,该对象不可用。

    

    java语言规范定义了以下几种对象可以作为GC Roots的对象:

    虚拟机栈(栈帧的本地变量表)中引用的对象;

    方法区中类静态属性引用的对象;

    方法区中常量引用的对象;

    本地方法栈中Native方法引用的对象;

java引用类型(4类)

    JDK1.2以后,java对引用类型进行了扩充,将引用分为Strong Reference、Soft Reference、Weak Reference、Phantom Reference 4种,这4种引用强度依次逐渐减弱。

    强引用(Strong Reference):程序代码普遍存在的,类似Object c = new Object(); 这类的引用,只要引用还在垃圾收集器永远不会回收掉被引用对象。

    软引用(Soft Reference):程序中有用但非必须的对象,对于软引用对象,在虚拟机内存将要发生溢出时,会将软引用对象列入回收范围进行二次回收,如果此次回收还没有足够内存才会抛出内存溢出异常。JDK 提供了java.lang.ref.SoftReference类来实现软引用。

    弱引用(Weak Reference):也是用来描述非必须对象,比软引用强度更弱一些,被弱引用关联对象只能生存到下一次垃圾收集前。当垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

    虚引用:最弱引用,对象是否关联弱引用对其生命周期完全不构成影响,仅仅是对象被垃圾收集器回收时收到一个通知。

    利用弱引用/软引用有效避免OOM

    转载至:http://www.cnblogs.com/dolphin0520/p/3784171.html

    java 4种引用我们常用的是强引用,另外三种使用最多的就是软引用和弱引用。在java应用程序中,可以使用软引用来实现图片缓存和网页缓存避免大量图片或网页缓存导致OOM的情况。

    对象死亡(回收)过程

    一个对象真正死亡,至少要经历两次标记过程:

    1、第一次标记:没有GC Roots引用链关联,将被第一次标记

    2、筛选:按照对象是否有必要执行的finalize()方法。当对象没有覆盖finalize方法或finalize方法已经被调用过,则虚拟机认为没必要执行。如果判断有必要执行finalize方法,继续步骤3

    3、放置进F-Queue队列:将需要执行finalize()对象放入F-Queue队列,随后JVM自动创建一个低优先级的FInalizer线程去执行,需要注意的是JVM仅是触发finalize()方法,并不会等待finalize()方法执行完成。

    4、对F-Queue队列对象再次标记

垃圾回收算法

标记-清除

    最基础收集算法,算法分为“标记”和“清除”两部分,其他算法都是基于它的思路并对其不足进行改进而得到的。标记-清除算法主要存在两个不足:效率问题,标记和清除过程效率都不高;空间问题,标记清除算法会大量产生不连续的内存空间,可能会导致创建较大对象时无法获取足够连续的内存而不得不提前触发另一次垃圾收集。

复制算法

    复制算法设计目标是为了解决回收效率问题。复制算法的思路是将内存分为两块相同大小空间,每次只使用其中的一块,这一块内存使用完了,先将存活的对象复制到另一块,然后把已使用空间一次性清除掉。复制算法实现简单、运行高效,但是代价是牺牲一半内存。

    现在商业虚拟机都采用复制算法回收新生代。MinorGc回收过程:复制-清空-互换

  •     首先、把Eden和Form Survivor区域存活对象复制到To Survivor区域(如果对象年龄达到老年代则将对象复制到老年代区),同时将这些存活对象年龄+1。如果To Survivor区域内存不足就存放到老年代,默认年龄达到15便移动到老年代;
  •     然后、清空Eden和From Survivor区域内存;
  •     最后、From 和 To Survivor区域互换,To Survivor作为下次Gc的From 区域。

    复制算法在对象存活率较高时会进行很多次复制,效率会变低。同时如果不希望内存利用率只有50%,需要额外的空间进行分配担保,以应对被使用内存所有对象100%存活的极端场景。所有老年代区域通常不采用复制算法。

标记-整理

    根据老年代特点,提出了标记-整理算法。标记过程和标记-清除算法一致,区别是标记后不立即清除,而是让存活对象向另一端移动,然后直接回收掉端边界以为的内存。

分代收集算法

    主流商业虚拟机都采用分代收集算法,将内存划分为不同年代区域,然后根据不同年代对象特点采用不同算法。对于对象死亡频率高的新生代区域采用复制算法(Minor Gc),对于对象存活率高的老年代采用标记-整理算法。

 垃圾收集器(基于HotSpot)

     垃圾收集器是内存回收的具体实现,java虚拟机规范对垃圾收集器具体实现没有做任何规定,不同厂商、不同版本的虚拟机提供的垃圾收集器可能会有较大差别。垃圾收收集器可以区分为新生代收集器、老年代收集器以及G1收集器。目前主流虚拟机还是使用GMS和G1收集器。

    

新生代收集器

    Serial

    Serial是最基本的收集器(历史最久远),单线程收集器,采用复制算法。在进行垃圾收集时,比较暂停其他所有工作线程,直到收集结束。

    Serial收集器由于其在单线程环境的简单和高效特性,其依旧是运行在Client模式下默认的收集器。

    ParNew收集器

    ParNew收集器是Serial的多线程版本,除了使用多线程进行垃圾收集外,其余包含所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器一样。需要注意ParNew收集器的多线程是指采用多线程进行垃圾收集,在执行收集任务时仍需要暂时所有用户线程。ParNew设计目标是通过多线程收集提高CPU的使用率,尽可能缩短垃圾收集的执行时间,从而缩短垃圾收集时用户线程停顿时间。

    ParNew收集器目前是大多数Sever模式虚拟机新生代首选的收集器,主要是因为目前除了Serial收集外,ParNew收集器是唯一能与CMS收集器配合工作的收集器。

    ParNew执行示意图:

    ParNew默认开启收集线程与实际的CPU数量相同,可以控制设置参数-XX:ParallelGCThreads 来限制允许的收集线程数,可以根据服务器实际情况进行调整。

    Parallel Scavenge 收集器

    Parallel Scavenge 收集器也是新生代的多线程收集器,采用复制算法。和ParNew不同的是,Parallel Scavenge 收集器设计的目标是达到一个可控制的吞吐量。吞吐量是指CPU用于运行用户代码的时间与CPU总时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

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

    Parallel Scavenge收集器可以通过参数精确控制吞吐量:

    -XX:MaxGCPauseMills 大于0的毫秒数,控制最大垃圾收集停顿时间,收集器将尽可能保证垃圾回收花费时间不超过设定值。

    -XX:GCTimeTatio 0-100之间的整数,垃圾收集时间占总时间比例,相当于吞吐量的倒数。例如设置为19,则允许的最大GC时间就占总时间的5%(1/(1+19)),默认值为99。

    -XX:+UseAdaptiveSizePolicy 开关参数,打开这个参数后,就可以打开GC自适应的调节策略——就不需要手工指定新生代大小(-Xmn)、Eden区和Survivor区比例、晋升老年代对象大小等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

老年代收集器

    Serial Old 收集器

    Serial Old 是Serial收集器的老年代版本,单线程收集器,采用标记-整理算法。该收集器目前主要意义也是作为Client模式下虚拟机使用。另外Serial Old收集器还是作为CMS收集器的后备预案,在并发收集出现Concurrent Model Failure时使用。

    Parallel Old 收集器

    Parallel Scavenge 收集器老版本,使用多线程和标记-整理算法,和Parallel Scavenge收集器组成吞吐量优先的垃圾收集应用组合。

    CMS 收集器

    CMS收集器设计目标是获取最短垃圾回收停顿时间为目标,目前主流垃圾收集器之一,大多数互联网和B/S服务端等客户线程停顿时间敏感的虚拟机优选收集器。CMS收集器基于标记-清除算法实现,CMS垃圾收集执行整个过程分为四个步骤:

  •     初始标记:仅仅是标记下GC Roots能关联到的对象,速度很快。初始标记需要Stop The World。
  •     并发标记:GC Roots Tracing 过程,该过程和用户线程并发执行。
  •     重新标记:修正并发标记过程用户线程并发执行导致标记产生变动的标记记录。重新标记需要Stop The World.
  •     并发清除:   清除标记后需要回收的内存,该过程和用户线程并发执行。

   从CMS收集过程整体来看,需要Stop The World的过程消耗时间都很短,需要花费较多时间的并发标记和并发清除过程都是与用户线程并发执行的,因此总体来说CMS收集器执行垃圾收集是和用户线程并发执行的。

    CMS收集器通过牺牲吞吐量和新生代空间为代价来尽可能缩短用户线程停顿时间。但是CMS并不完美,还存在如下缺点:

  •     CPU资源敏感,CMS在并发阶段虽然不会停顿用户线程,但是会因为占用部分线程而导致应用变慢,总的吞吐量降低。CMS默认启动的回收线程数=(CPU数量+3)/4。
  •     无法处理浮动垃圾,CMS在并发清理阶段,用户线程还在执行,自然会产生新的垃圾,这一部分垃圾出现在标记过程之后当次收集CMS无法处理,只能留待下次垃圾收集处理。因此CMS需要预留一部分空间提供并发收集时程序运作使用。CMS在JDK 1.6 以后启动阈值默认当老年代使用达到92%。当垃圾收集时,CMS预留内存无法满足程序需要,就会出现一次Concurrent Mode Failure,这时虚拟机将启动后备预案:临时启用Serial Old收集器重新进行老年代的垃圾收集,此时停顿时间反而变长。因此启动阈值设置过高将很容易出现Concurrent Mode Failure,性能反而降低。可以通过-XX:CMSInitiatiingOccupancyFraction 的值来设置触发百分比。
  •     会产生大量空间碎片,CMS基于标记-清理算法,收集结束时会有大量空间碎片产生。这会给大对象分配带来很大麻烦(根据内存分配策略,大对象将尽可能分配到老年代),如果无法找到足够大的连续内存空间将会提前触发一次Full GC。可以通过参数-XX:+UseCMSCompactAtFullCollection 开关参数(默认开启) 启动CMS收集在内存要顶不住进行Full GC时开启内存合并整理过程(需要注意内存合并整理过程也是Stop The World的,解决空间碎片问题带来的是用户线程停顿时间的增长)。可以通过参数-XX:CMSFullGCsBeforeCompaction 设置执行多少次不整理空间的Full GC后,下一次Full GC同时启动空间合并整理(默认0,每次Full GC都进行空间整理)。

    G1收集器

    JDK1.7 引入G1收集器,G1同样关注最小时延同时适合大尺寸堆内存的收集器,G1是一款面向服务端应用的垃圾收集器,官方也推荐G1代替CMS。G1引入分区的思路,弱化分代概念,合理利用垃圾收集各个周期的资源,解决了其他收集器众多缺点。

    G1收集器具有如下特点:

  •     并行与并发:G1能充分利用多CPU、多核环境的硬件优势,使用多个CPU来缩短Stop The World停顿的时间。
  •     分代收集:G1虽然弱化了分代概念,但仍保留了分代概念。和其他收集器分代的概念不同,G1没有了物理上年轻代和老年代的划分,也不是需要完全独立的Survivor堆区域做复制准备。G1只有逻辑上分代的概念,G1将整个堆划分为多个大小相同的独立区域(Regin),每个区域都可能随着G1运行进行切换。
  •     空间整合:与CMS标记-清除算法不同,G1从整体来说是基于标记-整理算法实现,从局部来看(两个Regin之间)是基于复制算法,G1在运作过程都不会产生空间碎片。   
  •     可预测停顿:G1相对于CMS一大优势就是能够建立可预测的停顿时间模型,可以让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。G1之所以可以建立可预测的停顿时间模型,是因为G1可以有计划的避免在整个java堆中进行全区域的垃圾收集。

    G1收集器运作过程大致可以分为:

  •     初始标记:标记GC Roots 并修改TAMS(Next Top at Mark Start)的值,该步骤执行时间很短,需要Stop The World。
  •     并发标记:对象可达性分析,找出存活对象,并发标记耗时较长,但是与用户线程并发执行。
  •     最终标记:修正并发标记过程因为用户线程执行导致标记状态改变的标记记录,这个阶段需要停顿,但是可以并行执行收集任务。
  •     筛选回收:和其他回收期区别的是,G1收集器在回收阶段不会一次性全部回收标记后的所有垃圾,而是先对所有Regin的回收价值和成本进行计算并排序,然后根据用户所期望的停顿时间来制定回收计划,从而达到可预估的停顿时间效果。

   垃圾收集器参数汇总

参  数参数设置范围描述
-XX:+UseSerialGC开关参数打开后使用Serial+Serial Old的收集器组合进行内存回收
-XX:+UseParNewGC开关参数打开后使用ParNew + Serial Old收集器组合进行内存回收
-XX:+UseConcMarkSweepGC开关参数打开后使用ParNew + CMS + Serial Old收集器组合进行内存回收。
Serial  Old将作为CMS 出现Concurrent Model Failure失败后备选方案。
-XX:+UseParallelGC开关参数虚拟机运行在Server模型下的默认值。打开后使用Parallel Scavenge + Serial Old收集器组合进行内存回收
-XX:+UseParallelOldGC开关参数打开后使用Parallel Scavenge + Parallel Old组合进行垃圾回收
-XX:+UseG1GC开关参数打开后使用G1进行垃圾回收
-XX:SurvivorRatio大于0整数新生代Eden区域和Survivor区域容量比值,默认为8,代表Eden:Survivo=8:1
-XX:PretenureSizeThreshold大于0整数设置直接晋升到老年代对象大小,单位k,当对象大小超过设值,直接进入老年代
注意:PretenureSizeThreshold参数仅对Serial和ParNew两款收集器有效。
-XX:MaxTenuringThreshold大于0整数设置晋升到老年代对象年龄,默认15,大于这个值得对象将复制到老年代
-XX:+UseAdaptiveSizePolicy开关参数动态调整java堆中各区域的大小及进入老年代的对象年龄
-XX:HandlePromotionFailure开关参数是否允许分配担保失败,即老年代的剩余空间不足以应付新生代整个Eden和Survivor区域对象都存活的极端情况。
-XX:ParallelGCThreads大于0整数设置并行收集时GC线程数
-XX:GCTimeRatio0-100之间的整数GC时间占CPU总时间比率,默认99,即允许GC时间=1/(1+99),仅Parallel Scavenge收集器时有效
-XX:MaxGCPauseMills大于0毫秒数设置GC最大停顿时间,仅Parallel Scavenge收集器时有效
-XX:CMSInitiatingOccupancyFration百分比数设置CMS收集器在老年代内存使用多少后触发垃圾收集,JDK1.7之前默认68%,JDK1.7及以后默认92%
-XX:+UseCMSCompactAtFullCollection开关参数默认开启,设置CMS收集器在垃圾收集后会开启空间整理
-XX:CMSFullGCsBeforeCompaction 设置CMS收集器在进行多少次不含空间整理的Full GC后开启一次带空间整理的Full GC,默认为0——每次Full GC都进行空间整理
-Xloggc:eclipse_gc.log 打印GC详细信息到日志文件eclipse_gc.log
+PrintGCTimeStamps 打印GC时间信息
-XX:+PrintGCDetails 打印GC详细信息
-XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照
XX:MaxDirectMemorySize=10M 设置直接内存区的最大容量为10M
-XX:MaxPermSize=10M 设置方法区的最大容量为10M
-XX:PermSize=10M 设置方法区的容量为10M
-Xss128k 设置虚拟机栈的大小为128k
-Xmn10M 设置新生代区的大小为10M
-Xmx20M 设置堆最大容量为20M
-Xms20M 设置堆最小容量为20M
-verbose:gc 输出虚拟机中GC的详细情况

    内存分配策略

    三大分配原则+空间担保:

  •     对象优先在Eden分配:大多数情况,对象在新生代Eden区中分配,Eden区内存不足够分配对象空间,虚拟机将进行一次Minor GC
  •     大对象直接进入老年代:大量连续内存空间的对象直接分配在老年代,典型的就是很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发下一次垃圾收集以获取连续空间来分配大对象内存。可以通过参数-XX:PretenureSizeThreshold设置直接进入老年代对象大小。程序开发过程尤其需要避免短命的大对象。
  •     长期存活对象进入老年代:虚拟机会给每个对象定义一个对象年龄(Age)计数器,每经过一次Minor GC后仍存活,并且能够被Survivor区域分配的话,将被移动到Survivor区域,并且年龄+1。当Age增加到一定年龄(-XX:MaxTenuringThreshold=15 设置,默认15),将将会晋升到老年代。
  •     空间分配担保:除了G1收集器,其他收集器组合,新生代收集都是采用复制算法,为了利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在经过Mino GC后仍然存活的情况下,需要老年代进行分配担保,前提是老年代还有剩余空间容纳这些对象。

 

    

    

   

    

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值