垃圾收集器与内存分配策略

1.概述
垃圾收集(Garbage Collection,GC).
当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。
Java内存运行时,程序计数器、虚拟机栈、本地方法栈三个区域随线程生,随线程灭;栈中的栈帧随方法的进入和退出,有条不紊的执行着出栈和入栈操作。每个栈帧分配的内存基本在类结构确定下来时就已知(尽管在运行期会由JIT编译器进行一些优化)。这几个区域不需过多考虑回收问题。
2.对象存活状态
堆中几乎存放着所有的对象实例,垃圾收集器在对堆进行回收前,首先要确定哪些对象还“存活”着,哪些对象已经“死去”(不可能再被任何途径使用)。
2.1引用计数算法
引用计数算法实现简单,判定效率高,如微软COM技术Python语言等都使用引用计数算法进行内存管理。
算法原理: 给对象添加一个引用计数器,每有一个地方引用他时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器值都为0的对象就是不可能再被使用的。
!!!!Java语言中没有选用引用计数算法来管理内存,主要原因是它很难解决对象之间的相互循环引用问题。!!!!
2.2根搜索算法
Java,C#及Lisp都是采用此算法判定对象是否存活。
基本思路: 通过一系列的名为”GC Roots“的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
这里写图片描述
Java中,可作为GC Roots的对象包括下面几种:
1)虚拟机栈(栈帧中的本地变量表)中的引用的对象;
2)方法区中的类静态属性引用的对象;
3)方法区中的常量引用的对象;
4)本地方法栈中JNI(Native方法)的引用的对象。
2.3再谈引用
Java有四种引用:
1)强引用: 在程序代码中普遍存在的,类似”Object obj = new Object( )”这类的引用。只要强引用还存在,则垃圾收集器永远不会回收掉被引用的对象。
2)软引用: 一些还有用,但并非必须的对象。对软引用关联着的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围中并进行第二次回收。若这次回收还是没有足够的内存,才会抛出内存溢出异常。SoftReference类实现软引用。
3)弱引用: 非必须对象,强度比软引用更弱一些,被若引用关联的对象只能生存到下一次垃圾回收之前。WeakReference类实现弱引用。
4)虚引用: 最弱的一种引用关系。无法通过虚引用来取得一个对象实例。为对象设置虚引用的唯一目的是希望在对象被垃圾收集器回收时收到一个系统通知。
PhantomReference类实现虚引用。
2.4生还是死?
根搜索算法中不可达的对象,并非是“非死不可”的。
真正宣告一个对象死亡,至少要经历两次标记过程:对象在跟搜索后没有与GC Roots的引用链,将会被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize( )方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将此两种情况都视为“没有必要执行”。
若对象被判定为有必要执行finalize( )方法,那么对象将会被放置在一个名为F-Queue的队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行。(对象的finalize()被执行,但仍可能存活。只要重新与引用链上的任何一个对象建立关联即可,那么第二次标记时将被移除出“即将回收”的集合。!!不推荐使用
2.5回收方法区
方法区(HotSpot虚拟机中的永久代)的垃圾收集主要回收两部分内容:废弃常量无用的类。回收效率低!
判定常量是否是“废弃常量”比较简单,
判断一个类是否是“无用的类”需同时满足下面3个条件:
1)类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
2)加载该类的ClassLoader已经被回收;
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足以上3个条件的无用类“可以”(不是一定会)被回收,不是和对象一样,不使用就必然会回收。
是否对类进行回收,HotSpot虚拟机提供了 -Xnoclassgc参数进行控制,还可使用-verbose:class及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类的加载和卸载信息。
在大量使用反射、动态代理、CGLib等bytecode框架的场景,及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,保证永久代不会溢出。
3.垃圾收集算法
3.1标记 — 清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它是最基本的收集算法,其他算法都是基于此思路并对其缺点进行改进得到。
缺点:1)效率低;2)会产生大量不连续内存碎片。
这里写图片描述
3.2复制算法
为了解决效率问题。
将内存按容量划分为大小相等的两块,每次只使用其中一块。当此块内存用完之后,将还存活着的对象复制到另一块,然后把已使用过的内存空间一次清理掉。使得每次只对其中一块内存进行回收,内存分配时不用考虑内存碎片等情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
代价:将内存缩小为原来的一半。
这里写图片描述
现在商业虚拟机都采用这种算法回收新生代
HotSpot将内存分为一块较大的Eden空间两块较小的Survivor空间,每次使用Eden和其中一块Survivor。回收时,将Eden和Survivor中还存活的对象一次性拷贝到另外一块Survivor空间,最后清理掉Eden和Survivor中空间。
HotSpot虚拟机默认Eden和Survivor比例为8:1,只有10%空间被“浪费”。当Survivor空间不够用时,需依赖其他内存(此处指老年代)进行分配担保
复制算法在对象存活率较高时要执行较多的复制操作,效率会变低。
3.3标记 — 整理算法
标记出需要回收的对象之后,不是直接清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
这里写图片描述
3.4分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法。
原理: 根据对象存活周期不同,将内存划分为几块。一般把Java堆分为新生代老年代,根据各年代的特点采用最适当的收集算法。
新生代—>复制算法;
老年代—>标记清除/标记整理
4.垃圾收集器
收集算法视为内存回收方法论;垃圾收集器视为内存回收具体实现。
讨论基于Sun HotSpot虚拟机1.6版Update22的收集器:
这里写图片描述
上图展示了7种作用于不同分代的收集器(包括JDK 1.6_Update14后引入的Early Access版G1收集器),若两收集器之间连线,说明可搭配使用。
4.1Serial收集器 —>新生代收集
它是一个单线程收集器,不仅是只会使用一个CPU一条收集线程去完成垃圾收集工作,更重要的是在它工作时必须暂停其他所有的工作线程(Stop the world),直到他收集结束。此工作是由虚拟机在后台自动发起和自动完成的。
它是现在为止,虚拟机运行在Client模式下的默认新生代收集器。它与其它收集器的单线程比,没有线程交互的开销,简单而高效。
这里写图片描述
适用性Serial收集器对于运行在Client模式下的虚拟机来说是很好的选择
4.2ParNew收集器 —>新生代收集
其实是Serial收集器的多线程版本。
Serial收集器可用的所有控制参数(-XX:SurvivorRatio、-XX:PrenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。
这里写图片描述
适用性许多运行在Server模式下虚拟机中首选的新生代收集器
除Serial收集器外,目前只有它能与CMS收集器(并发收集器、老年代收集器)配合工作。
它默认开启的收集线程数与CPU数量相同。可以使用-XX:ParallelGCThreads参数来限制垃圾收集线程数。
并行(Parallel):多条垃圾收集线程并行工作,但此时用户线程处于等待状态;
并发(Concurrent):用户线程与垃圾收集线程同时执行(不一定并行,可能交互执行),用户程序继续运行,而垃圾收集程序运行于另一个CPU上。)
4.3Parallel Scavenge收集器 —>新生代收集
也是使用复制算法的收集器,又是并行的多线程收集器。
独有的特点是,收集器目标是达到一个可控制的吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
高吞吐量主要适合在后台运算而不需要太多交互的任务。
提供的用于精确控制吞吐量的两个参数:1)控制最大垃圾收集停顿时间的-XX:MaxGCPaulseMillis参数;2)直接设置吞吐量大小的-XX:GCTimeRatio参数。
MaxGCPaulseMillis参数允许值是一个大于0的毫秒数;GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。
??? GCTimeRatio参数值是一个大于0小于100的整数(垃圾收集时间占总时间的比率)。若设置为19,则允许的最大GC时间占总时间的1/(1+19).
GC自适应调节策略:打开-XX:+UseAdaptiveSizePolicy开关参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大吞吐量。(不再需要手工指定新生代大小(-Xmn)、Eden与Survivor区比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数)
只需把基本内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPaulseMillis或-XX:GCTimeRatio参数给虚拟机设立一个优化目标,具体细节参数的调节工作交由虚拟机完成。
4.4Serial Old收集器 —>老年代
Serial收集器的老年代版本,同是一个单线程收集器,使用“标记-整理”算法。
主要意义也是被Client模式下虚拟机使用。
这里写图片描述
4.5Parallel Old收集器(JDK1.6中开始提供) —>老年代
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理算法”。
适用性:在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
这里写图片描述
4.6CMS收集器 —>老年代
CMS(Concurent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现。
目前很大一部分Java的应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务响应速度,希望系统停顿时间最短,有较好使用体验。CMS收集器非常符合这类应用需求。
运作过程可分为4步:初始标记—>并发标记—>重新标记—>并发清除
初始标记和重新标记两个步骤仍需”Stop The World”.初始标记仅标记一下GC Roots能直接关联到的对象,速度很快;并发标记就是进行GC Roots Tracing的过程;重新标记则是为了修正并发标记期间,因用户程序继续运行而导致标记产出变动的那一部分对象的标记记录,这阶段停顿时间一般比初始标记阶段长,但远比并发标记时间短。
整个过程耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
这里写图片描述
4.7G1收集器
与CMS收集器相比的两个显著改进:1)G1收集器基于“标记-整理”算法,不会产生空间碎片;2)可非常精确的控制停顿,能让使用者明确指定在一个长度为M毫秒的时间片内,消耗在垃圾收集上的时间不超过N毫秒,几乎是实时Java(RTSJ)的垃圾收集器特征。
它可实现基本不牺牲吞吐量前提下完成低停顿的内存回收(由于能极力避免全区域垃圾收集,之前收集器都是针对整个新生代或老年代。G1将Java堆划分为多个大小固定的独立区域,并跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间优先回收垃圾最多的区域)。
区域划分以及有优先级的区域回收,保证了G1收集器在有限时间内可获得最高收集效率。
4.8垃圾收集器参数总结
这里写图片描述
5.内存分配与回收策略
Java技术体系中自动内存管理自动化的解决了两个问题:1)给对象分配内存;2)回收分配给对象的内存。
对象内存分配,大方向讲,就是在上分配(也可能经JIT编译后被拆散为标量类型并间接在栈上分配),对象主要分配在新生代Eden区上,如果启动本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况会直接分配在老年代,细节取决于使用哪种垃圾收集器组合,还有虚拟机中与内存相关参数配置。
5.1对象优先在Eden分配
当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
虚拟机提供-XX:+PrintGCDetails收集器日志参数,让虚拟机在发生垃圾收集行为时打印内存回收日志,并在进程退出时输出当前内存各区域分配情况。
新生代GC(Minor GC):发生在新生代的垃圾收集动作,因Java对象多具有朝生夕灭特性,Minor GC非常频繁,一般回收速度也较快。
老年代GC(Major GC/Full GC):出现Major GC经常会伴随至少一次的Minor GC(并非绝对,在ParallelScavenge收集器的收集策略里,就有直接进行Major GC的策略选择过程)。Major GC速度一般比Minor GC慢10倍以上。
5.2大对象直接进入老年代
大对象指,需大量连续内存空间的Java对象。
经常出现大对象易导致内存还有不少空间时就提前触发垃圾收集以获取足够连续空间来“安置”它们。
虚拟机提供-XX:PretenureSizeThreshold参数(只对Serial和ParNew两款收集器有效),令大于这个值的对象直接在老年代分配(避免在Eden区及两个Survivor区之间发生大量内存拷贝)。
5.3长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。若对象在Eden出生并经第一次Minor GC后仍存活,并能被Survivor容纳,将被移动到Survivor空间,并将对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄增加1岁当年龄增加到一定程度(默认15)时,就会被晋升到老年代中。对象晋升老年代阈值可通过参数-XX:MaxTenuringThreshold来设置。
5.4动态对象年龄判定
为能更好适应不同程序内存状况,虚拟机并不总要求对象年龄必须达到MaxTenuringThreshold才晋升老年代。若Survivor空间中相同年龄所有对象大小的总和*大于Survivor空间的一半*,则大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold要求的年龄。
5.5空间分配担保
发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,若大于则直接进行一次Full GC。若小于,则查看HandlePromotionFailure设置是否允许担保失败,若允许则只会进行Minor GC;若不允许,则改为进行一次Full GC。
当出现大量对象在Minor GC后仍存活的情况时(最极端就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代(前提是老年代本身还有容纳这些对象的剩余空间)。
如果担保失败(HandlePromotionFailure),就之后在失败后重新进行一次Full GC。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值