四、垃圾回收器
1、垃圾回收算法
【1】标记-清除算法(Mark-Sweep)
垃圾回收器先判定出所有需要回收的对象,并标记,然后再执行清理工作,把刚才标记出来的对象都回收掉
缺点:一是效率不高,二是会产生内存碎片
【2】复制算法(Copying)
将经过垃圾回收器工作后的,仍然存活的对象,从执行垃圾回收工作的内存区域,复制到另外一块空闲的内存区域当中
优点:不会产生内存碎片
缺点:浪费内存区域
【3】标记-整理算法(Mark-Compact)
垃圾回收器先判定出所有需要回收的对象,并标记,然后将不需要回收的对象(未被标记的)向内存空闲的一端移动,最后执行垃圾回收操作
优点:不会产生内存碎片
缺点:效率不高
【4】分代收集算法(Generational Collection)
根据对象的存活周期的不同,将内存划分为几块,一般是把Java堆分为新生代和老年代。这样就可以根据各个年代的特点采用最适合的收集算法
新生代,对象的存活周期较短,适合使用复制算法
老年代,对象的存活周期较长,没有额外空间对其进行分配担保,所以适合使用标记-清除或标记-整理算法
2、Java垃圾收集器
【1】Serial
串行收集器
在新生代和老年代均有且只有一个收集线程执行垃圾回收操作。在收集线程进行垃圾回收时,必须暂停其他的工作线程,直到它收集结束(STW,Stop The World)
新生代采用单线程工作,使用复制算法;老年代采用单线程工作,使用标记-整理算法
【2】ParNew
新生代并行收集器
它和SerialGC相比,它优化点在于新生代采用并行收集线程来工作。它默认开启的收集线程数和CPU的数量是一致的,可以通过-XX:ParallelGCThreads
参数来限制并行收集的线程数量
新生代采用并行收集线程工作,使用复制算法;老年代采用单线程工作,使用标记-整理算法
【3】Parallel Scavenge
新生代并行收集器
和ParNew相比,它的目标是减少STW的时间,达到一个可控制的吞吐量(Throughput)
吞吐量就是CPU用于执行用户代码的时间与CPU总的消耗时间(执行用户代码的时间 + GC时间)的比值。比如虚拟机运行了100分钟,其中垃圾回收消耗1分钟,则吞吐量就是 (100 - 1) / 100 * 100% = 99%
可以通过-XX:MaxGCPauseMillis=???
来控制最大停顿时间,
以及通过-XX:GCTimeRatio=???
来控制吞吐量的大小
此外还有一个参数-XX:+UseAdaptiveSizePolicy
,打开它后,JVM可以根据当前系统的运行情况,收集性能监控信息,动态地调整诸如:-Xmn(新生代大小)、-XX:SurvivorRatio(伊甸园区和幸存区的比例)和-XX:PretenureSizeThreshold(晋升老年代对象年龄)等参数
新生代采用并行收集线程工作,使用复制算法;老年代采用单线程工作,使用标记-整理算法
【4】Serial Old(PS MarkSweep)
老年代串行收集器
现在主要用于给CMS收集器做后备预案使用,即在发生Concurrent Mode Failure时使用
它在新生代和老年代仅使用一个收集线程来执行垃圾回收操作
Parallel Scavenge的老年代收集器使用PS MarkSweep,但是其实现和Serial Old的实现非常接近,所以在官方的很多资料中,都是用Serial Old来代替PS MarkSweep来进行讲解的
新生代采用单线程工作,使用复制算法;老年代采用单线程工作,使用标记-整理算法
【5】Parallel Old
并行收集器
新生代采用并行收集线程工作,使用复制算法;老年代采用并行收集线程工作,使用标记-整理算法
【6】CMS(Concurrent Mark Sweep)
并发标记-清理收集器
它是一种以获取最短STW时间为目标的收集器
老年代采用并行标记线程和并行收集线程工作,使用标记-清除算法
工作过程:
-
初始标记(CMS initial mark)【STW】
标记出GC Roots能直接关联的对象
-
并发标记(CMS concurrent mark)【和用户线程一起工作】
进行GC Roots Tracing
-
重新标记(CMS remark)【STW】
修正并发标记期间,因用户程序继续运作,而导致标记产生变动的,那一部分对象的标记记录
这个阶段的执行时间大于初始标记的执行时间,小于并发标记的执行时间
-
并发清除(CMS concurrent sweep)【和用户线程一起工作】
缺点:
-
CMS对CPU资源非常敏感。其实面向并发设计的程序对CPU资源都很敏感。虽说在并发标记阶段,它不会STW,但是会因为占用了一部分线程(CPU资源),而导致程序变慢,降低总的吞吐量
默认启动的回收线程数是 (CPU数量 + 3) / 4,当CPU数量是4个以上时,并行回收垃圾的收集线程不少于25%,而当CPU数量不足4个时,则需要拿出一半的运算能力,给CMS去执行收集线程
-
CMS无法清理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure,而导致Full GC产生
浮动垃圾就是指在并发标记阶段之后出现的新的垃圾对象,这些对象只能等到下一次GC的时候被回收掉
-
CMS无法像其他收集器一样,等老年代几乎满了,才进行收集。它需要预留一部分空间给并行执行的程序运作时使用。可以通过
-XX:CMSInitiatingOccupancyFraction
来调整触发百分比。该属性默认值:JDK5是68,JDK6是92,JDK7是-1(JDK7还有另外一个参数CMSInitiatingPermOccupancyFraction
,对应的是永久代的回收触发内存比例),JDK8是-1如果预留的内存空间无法满足程序的需要,就会出现Concurrent Mode Failure,这时JVM将启动后备预案,临时启动Serial Old来执行垃圾回收。但是这样会加长STW的时间。所以预留的内存空间不宜过小
-
CMS使用标记-清除算法,就会产生内存碎片。内存碎片过多,会给大对象的分配造成很大麻烦。虽然老年代有很多剩余空间,但是由于无法找到足够的连续的内存空间,就会触发Full GC
有一个参数
-XX:+UseCMSCompactAtFullCollection
,用于控制当Full GC时,对内存进行整理操作。但是内存整理操作是无法并行的。还有一个参数-XX:CMSFullGCsBeforeCompaction
,用于设置当执行多少次没有进行内存整理的Full GC后,紧接着来一次压缩的Full GC,默认为0,即每次Full GC都压缩
【7】G1
Garbage-First,是一款面向服务端应用的垃圾收集器
G1将整个Java堆划分为多个大小相等的区域(Region)。和以往不同的是,虽然它保留了新生代和养老代的概念,但是新生代和养老代却不再是物理上连续的了,而是每个Region当中都包含新生代和养老代的一部分
默认将Java堆分为2048左右个Region(如果数量太少,会影响收集效率,增加扫描的时间)。可以通过参数-XX:G1HeapRegionSize=???
来调整每个分区的大小,默认为1MB(1048576B),最大32MB(33554432B),且必须是2的幂次方,不足1MB,取1MB,大于1MB,向下取2的幂次方值
G1还可以有计划地避免在整个Java堆中进行全Region的垃圾收集,它会跟踪各个Region里的垃圾堆积的价值大小(GC可以释放的内存大小和所需要的收集时间),在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的Region(Garbage-First名称的由来)
特点:
-
并行与并发
充分利用多核CPU的硬件优势,减少STW的时间,提高吞吐量。并且垃圾收集线程和用户线程可以并发处理
-
分代收集
G1将每个Region逻辑划分为Eden、Survivor和Old。每个Region会随着G1收集器的运行,而不断地调整切换
-
空间整合
G1从整体上看,是使用标记-整理算法,但从局部看,是基于复制算法实现的。这就意味着在G1收集器运作期间,不会产生内存碎片。不会因为程序需要分配大对象时,找不到连续的内存空间,而提前触发下一次GC
-
可预测的停顿
可以通过参数
-XX:MaxGCPauseMillis
来指定最大STW时间。并根据制定的回收优先级列表,优先回收那些回收价值大的Region
工作过程:
-
初始标记(initial Marking)【STW】
标记GC Roots能直接关联到的对象。执行时间较短
-
并发标记(Concurrent Marking)【和用户线程一起工作】
从GC Roots开始,对堆内存中的对象进行可达性分析,找出存活的对象。执行时间较长,但可以和用户程序并发执行
-
最终标记(Final Marking)【STW】
修正因程序并发执行而导致标记发生变动的记录。可以和用户程序并行执行
-
筛选回收(Live Data Counting and Evacuation)
根据用户指定的停顿时间,制定收集计划,并执行收集命令。可以和用户程序并行执行
注意:就目前而言,G1收集器并不稳定,不推荐在生产系统上使用它
3、垃圾收集器的选择
【1】JDK源码
bool Arguments::check_gc_consistency() {
bool status = true;
uint i = 0;
if (UseSerialGC) i++;
if (UseConcMarkSweepGC || UseParNewGC) i++;
if (UseParallelGC || UseParallelOldGC) i++;
if (UseG1GC) i++;
if (i > 1) {
jio_fprintf(defaultStream::error_stream(),
"Conflicting collector combinations in option list; "
"please refer to the release notes for the combinations "
"allowed\n");
status = false;
}
return status;
}
在JDK的C++源码中,有个方法用于检查GC策略配置的正确性
【2】Serial + Serial Old
使用参数-XX:+UseSerialGC
新生代使用串行收集器,老年代也使用串行收集器
【3】ParNew + Serial Old
使用参数-XX:+UseParNewGC
新生代使用并行收集器,老年代使用串行收集器
注意使用该策略配置,虚拟机会抛出警告信息
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
建议不要使用该策略配置,未来发行版本可能会移除它
【4】ParNew + (CMS + Serial Old)
使用参数-XX:+UseConcMarkSweepGC
新生代使用并行收集器,老年代使用并行-清除收集器(串行收集器做后备预案)
【5】Parallel Scavenge + Parallel Old
使用参数-XX:+UseParallelGC
或 -XX:+UseParallelOldGC
新生代使用并行收集器,老年代也使用并行收集器
JDK8默认GC策略配置
【6】G1
使用参数-XX+:UseG1GC
【7】总结
单核:Serial
多核,大吞吐量:Parallel Scavenge 和 Parallel Old
多核,快速响应:ParNew 和 CMS
4、Java对象的内存分配
-
优先在Eden(新生代的伊甸园区)进行分配
-
大对象直接进入老年代。所谓的大对象就是指需要大量连续的内存空间的对象,例如很长的字符串或大数组。可以通过
-XX:PretenureSizeThreshold=???
参数来调节对象大小的阈值,默认为0,即不限制。这个参数不带单位。注意该参数仅对Serial和ParNew两种收集器有效,Parallel Scavenge收集器不认识该参数 -
对象的存活次数达到MaxTenuringThreshold时,将进入老年代
-
如果幸存区中相同年龄的所有对象的大小总和大于Survivor(幸存区域)的一半,则年龄大于或等于该年龄的对象会在Minor GC时被复制到老年代。这就是动态对象年龄判断
-
当Minor GC发生时,如果伊甸园区和某个幸存区中存活的对象,无法复制到另外一个幸存区时,就会通过空间分配担保机制,向老年代借用内存,提前将对象复制到老年代中
可以通过
XX:+HandlePromotionFailure
参数来允许担保失败如果允许担保失败,将会检查老年代最大可用连续内存是否大于晋升至老年代的对象的平均大小,如果大于,将会尝试进行Minor GC,否则就进行Full GC。这里进行Minor GC是存在风险的,因为可能存在存活的大对象,导致其晋升至老年代,而老年代没有足够的内存空间,从而导致Full GC
如果不允许担保失败,则直接进行Full GC