本文主要介绍常见的java垃圾收集器、如何阅读jvm日志和虚拟机中内存的分配策略。
一、垃圾收集器
垃圾收集器可以是看做内存回收的具体实现。Java虚拟机规范对垃圾收集器并未做任何规定,所以不同厂商、不同版本虚拟机提供的垃圾收集器有很大不同。版本主要讨论HotSpot虚拟机的垃圾回收器。
本图为常见的七种垃圾回收器,上半部分为年轻代的回收器,下半部分为老年代的回收器,G1比较全能,上可回收年轻代,下可处理老年代。接下来我们对各收集器进行介绍。
年轻代收集器
1.Serial收集器
这是一个古老的垃圾回收器,使用复制算法。Serial是一个单线程的收集器,这个“单线程”并不是仅仅说明它只会使用一个CPU或一条线程去完成垃圾收集工作,更重要的是在其进行垃圾回收时需要暂停所有其他进程来进行垃圾回收工作(Stop The World)。
Stop The World会暂停全部线程,给用户带来不良的体验,虚拟机开发团队不断的努力消除停顿时间,我们也看到了一个个的优秀回收器出现,停顿时间也越来越断,但仍无法完全消除。
与其他垃圾回收器相比:Serial简单而高效,对于限定单个CPU的环境来说,Serial不需要进行线程交互,专心捡垃圾吃,效率极高,一般是Client模式下一个很好的选择。
2.ParNew收集器
ParNew是Serial的多线程版本,在GC时使用多线程进行回收,除此之外,包括所有控制参数,收集算法,Stop The World,对象分配规则,回收策略等都与Serail收集器一致。
但ParNew是Server模式下虚拟机的首选新生代服务器,因为 Parallel收集器无法与CMS配合使用,而在多核CPU的情况下Serial的效率不如ParNew。
并发(Concurrent):用户线程与垃圾回收线程同时执行,可能交替执行,用户程序可能和垃圾收集线程运行在不同的CPU上。
3.Parallel Scavenge收集器
Parallel Scavenge是一个新生代的收集器,使用复制算法,也是一个并行多线程的收集器,不过Parallel Scavenge的目的是达到一个可控制的吞吐量(Throughput)。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)。停顿时间越短越适合需要与客户交互的程序,高吞吐量可以高效运用CPU时间。
两个参数:-XX:MaxGCPauseMillis
设置最大停顿时间,-XX:GCTimeRation
设置吞吐量的大小。但并不是停顿时间越短越好,如果时间过断,会导致频繁出发垃圾回收,吞吐量也会随之下降。
另外,Parallel Scavenge收集器还能根据情况自动调节策略,对于对收集器不太了解的可以开启PS收集器的自动调节功能。-XX:+UseAdaptiveSizePolicy
老年代收集器
1.Serial Old收集器
是Serial的老年代版本,使用“标记—整理”算法。这个收集器同样也是单线程的,也是给Client模式下的虚拟机使用的。
不过,Serial Old还有其他两个用途:一、JDK1.5之配合Parallel Scavenge使用 ;二、作为CMS的后备方案,在并发收集发生Concurrent Mode Failure时使用。
2.Parallel Old收集器
是Parallel Scavenge收集器的老年代版本。使用多线程的标记—整理算法。在JDK1.6中提供,在此之前Parallel Scavenge只能配合Serial Old使用,但Serial在服务端性能并不优秀,所以并不能发挥Parallel Scavenge吞吐量最大化的效果。
直到Parallel Old出现后,“吞吐量优先”收集器才有了名符其实的组合,在注重吞吐量及CPU资源敏感的场合都可以考虑Parallel Scavenge+Parallel Old组合。
3.CMS收集器(Concurrent Mark Sweep)
是一种获取最短回收停顿时间为目的的收集器。使用标记—清除算法。适用于注重相应时间的互联网站或B/S系统的服务端。
标记过程:
- 初始标记(initial mark)
- 并发标记(concurrent mark)
- 重新标记(remark)
- 并发清除(concurrent sweep)
初始标记和重新标记仍需要“stop the world”,不过这两个过程与并发标记相比,所用时间都极短。但用时较长的并发标记和并发清除都可以与用户进程同时进行。
- 对CPU资源十分敏感,虽不会导致用户线程停顿,但会因占用一部分线程使应用变慢,吞吐量下降。默认启动回收线程为(cpu数量+3)/4,至少使用25%的资源,对于少于4核的处理器简直是灾难。
- 无法处理浮动垃圾,在CMS清除的过程中用户程序仍在运行,仍会产生垃圾,会在下次GC时清除,需要预留足够空间给用户线程使用。如果预留内存无法满足要求,则会出现Concurrent Mode Failure,需要临时使用Serial Old进行回收,这样就会降低效率
- 标记清除本身的缺点,大量的碎片化空间会提前触发GC,解决方法是在进行Full GC前进行一次压缩,如果仍不够用再进行Full GC
G1收集器 (Garbage-First)
G1是面向服务端的垃圾回收器,出现的意义是替换掉JDK1.5发布的CMS收集器。
特点:
- 并行与并发:充分利用CPU,多核环境,使用多CPU来减少Stop The World时间。
- 分代收集:在G1中分代收集仍被保留,不需要与其他处理器配合。
- 空间整理:与CMS的“标记—清除”不同,G1从整体上看是基于“标记—整理”,但从局部看是使用“复制算法”,避免产生内存碎片。
- 可预测停顿:追求低停顿,并可以建立可预测的停顿时间模型。
G1收集器的堆内存划分与其他收集器不同,会将堆划分成多个大小相等的独立区域(Region),在Region中会有年轻代和老年代,G1会优先回收价值大的Region,而不是对整个堆进行回收。虚拟机一般会使用Remembered Set避免进行全堆扫描,G1中每个Region都会有一个Remebered Set。
运作过程:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Date Counting and Evacuation)
二、理解GC日志
jdk1.7
1 | [GC [PSYoungGen: 7926K->480K(153600K)] 7926K->480K(502784K), 0.0014400 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] |
jdk1.8
1 | [GC (System.gc()) [PSYoungGen: 5263K->608K(153088K)] 5263K->616K(502784K), 0.0015586 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] |
[GC和[Full GC是用来区分垃圾回收的类型,而不是区分年轻代GC还是老年代GC,Full GC代表发生了“Stop The World”。(System.gc())代表调用System.gc()触发垃圾回收。年轻代GC是Minor GC,老年代的GC是Major GC,Full GC是对整个堆内存进行GC。
PSYoungGen、ParOldGen、PSPermGen代表的是垃圾回收的内存区域,与使用的垃圾回收器相关。
垃圾回收器 | 年轻代 | 老年代 | 永久代 |
---|---|---|---|
Serial | DefNew | Perm | |
Serial Old | Tenured | Perm | |
ParNew | ParNew | Perm | |
CMS | CMS | CMS Perm | |
Parallel Scavenge | PSYoungGen | PSPermGen | |
Parallel Old | ParOldGen | PSPermGen |
在jdk1.8中永久代全部换成了元空间Metaspace~
在方括号内部的5263K->608K是指该区域内存已用容量->该区域GC内存使用量,在方括号外的5263K->616K是指GC前java堆使用容量->GC后堆内存使用量。0.0015586 secs代表本次GC消耗时间,单位是秒。
[Times: user=0.03 sys=0.00, real=0.00 secs] ,分别是用户态消耗CPU时间、内核态消费CPU时间和操作开始到操作结束经过的墙钟时间(Wall Clock Time);墙钟时间包括各种非运算时间,IO、线程阻塞,CPU耗时则不包括这些,但如果是多线程或者是多核则会将这些时间叠加,所以CPU时间大于真实时间也很正常。
三、内存分配策略
java虚拟机自动管理内存归结为两个问题:给对象分配内存和回收分配给对象的内存。
三个新生代的收集器都是使用复制算法。复制算法中将内存分为三个区域,一个Eden区,两个Survivor区,默认比例是8:1:1,可以使用-XX:SurvivorRatio=8
来进行配置,代表Eden:Survivor=8:1。接下来以Serial/Serial old为例进行讲述。
对象优先在Eden区进行分配,如果Eden区空间不够会进行一次Minor GC。
Minor GCMinor GC时,会将存活的对象都放入到另一个Survivor(右)中,新来的对象会继续在eden中进行分配,放不下的时候进行MinorGC,把存活的对象放入另一个不用的Survivor(左)中。
长期存活的对象进入老年代存活的对象都会有一个年龄,记录经历了几轮GC,当达到一定年龄后就会将对象放入到老年代中。默认晋升年龄阈值为15,可以使用-XX:MaxTenuringThreshold=15
来进行配置。
如果新来的是个大对象,可能会直接进入老年代,大对象在年轻代会占据连续的空间,尤其是的短命的大对象,容易引起内存还剩不少时就出发垃圾回收。长寿命的大对象也有问题,在Eden和Survivor之间来回复制会发生大量的内存复制,这会大大降低复制算法的效率。所以一般来将,大对象直接进入老年代。对于Serial和ParNew收集器,可以使用-XX:PretenureSizeThreshold=121241
来配置多大的对象是大对象,单位是k。
当Survivor中1岁啦,2岁啦的对象超过一半时会直接进入老年代。
空间分配担保在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果条件成立,则表明本次GC是安全的,即使全都进了老年代还装的下!
如果不成立,查看+XX:-HandlePromotionFailure
设置是否担保失败。如果允许,需要判断老年代连续空间是否大于历次晋升到老年代的平均值,那么尝试Minor GC,尽管存在风险,如果小于或未配置这个参数,需要进行一次Full GC。
风险指的是复制算法收集存活的对象如果大于Survivor能容纳的对象,会直接进入老年代,如果老年代没有足够的空间就会出发Full GC进行垃圾回收。尽管如此,只要连续空间大于平均值,就有很大的概率不会进入Full GC,这样就能有效的提高性能。
四、总结
本文介绍了常见的集中垃圾回收器、GC日志的理解和Java的内存分配和回收策略,这些都是jvm知识体系中很重要的内容,有必要我们去仔细研究!
参考资料: